agenthub-python 0.2.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agenthub/auto_client.py CHANGED
@@ -46,22 +46,26 @@ class AutoLLMClient(LLMClient):
46
46
  ) -> LLMClient:
47
47
  """Create the appropriate client for the given model."""
48
48
  client_type = client_type or os.getenv("CLIENT_TYPE", model.lower())
49
- if "gemini-3" in client_type: # e.g., gemini-3-flash-preview
49
+ if "gemini-3-" in client_type or "gemini-3.1-" in client_type: # e.g., gemini-3-flash-preview
50
50
  from .gemini3 import Gemini3Client
51
51
 
52
52
  return Gemini3Client(model=model, api_key=api_key, base_url=base_url)
53
- elif "claude" in client_type and "4-5" in client_type: # e.g., claude-sonnet-4-5
54
- from .claude4_5 import Claude4_5Client
53
+ elif "claude" in client_type and "4-6" in client_type: # e.g., claude-sonnet-4-6
54
+ from .claude4_6 import Claude4_6Client
55
55
 
56
- return Claude4_5Client(model=model, api_key=api_key, base_url=base_url)
57
- elif "gpt-5.1" in client_type or "gpt-5.2" in client_type: # e.g., gpt-5.2
58
- from .gpt5_2 import GPT5_2Client
56
+ return Claude4_6Client(model=model, api_key=api_key, base_url=base_url)
57
+ elif "gpt-5.4" in client_type or "gpt-5.5" in client_type: # e.g., gpt-5.5
58
+ from .gpt5_5 import GPT5_5Client
59
59
 
60
- return GPT5_2Client(model=model, api_key=api_key, base_url=base_url)
61
- elif "glm-4.7" in client_type: # e.g., glm-4.7
62
- from .glm4_7 import GLM4_7Client
60
+ return GPT5_5Client(model=model, api_key=api_key, base_url=base_url)
61
+ elif "glm-5" in client_type:
62
+ from .glm5 import GLM5Client
63
63
 
64
- return GLM4_7Client(model=model, api_key=api_key, base_url=base_url)
64
+ return GLM5Client(model=model, api_key=api_key, base_url=base_url)
65
+ elif "kimi-k2.5" in client_type:
66
+ from .kimi_k2_5 import KimiK2_5Client
67
+
68
+ return KimiK2_5Client(model=model, api_key=api_key, base_url=base_url)
65
69
  elif "qwen3" in client_type:
66
70
  from .qwen3 import Qwen3Client
67
71
 
@@ -69,7 +73,7 @@ class AutoLLMClient(LLMClient):
69
73
  else:
70
74
  raise ValueError(
71
75
  f"{client_type} is not supported. "
72
- "Supported client types: gemini-3, claude-4-5, gpt-5.2, glm-4.7, qwen3."
76
+ "Supported client types: gemini-3, claude-4-6, gpt-5.4, gpt-5.5, glm-5, kimi-k2.5, qwen3."
73
77
  )
74
78
 
75
79
  def transform_uni_config_to_model_config(self, config: UniConfig) -> Any:
@@ -84,6 +88,13 @@ class AutoLLMClient(LLMClient):
84
88
  """Delegate to underlying client's transform_model_output_to_uni_event."""
85
89
  return self._client.transform_model_output_to_uni_event(model_output)
86
90
 
91
+ async def _streaming_response_internal(
92
+ self,
93
+ messages: list[UniMessage],
94
+ config: UniConfig,
95
+ ) -> AsyncIterator[UniEvent]:
96
+ raise NotImplementedError("Please use streaming_response instead.")
97
+
87
98
  async def streaming_response(
88
99
  self,
89
100
  messages: list[UniMessage],
@@ -115,3 +126,7 @@ class AutoLLMClient(LLMClient):
115
126
  def get_history(self) -> list[UniMessage]:
116
127
  """Get history from the underlying client."""
117
128
  return self._client.get_history()
129
+
130
+ def set_history(self, history: list[UniMessage]) -> None:
131
+ """Set history in the underlying client."""
132
+ self._client.set_history(history)
agenthub/base_client.py CHANGED
@@ -12,6 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import time
15
16
  from abc import ABC, abstractmethod
16
17
  from typing import Any, AsyncIterator
17
18
 
@@ -84,21 +85,31 @@ class LLMClient(ABC):
84
85
  content_items: list[ContentItem] = []
85
86
  usage_metadata: UsageMetadata | None = None
86
87
  finish_reason: FinishReason | None = None
88
+ created_at: int | None = None
87
89
 
88
90
  for event in events:
89
91
  # Merge content_items from all events
90
92
  for item in event["content_items"]:
91
93
  if item["type"] == "text":
92
- if content_items and content_items[-1]["type"] == "text":
94
+ if (
95
+ content_items
96
+ and content_items[-1]["type"] == "text"
97
+ and content_items[-1].get("signature") is None # no signature yet
98
+ and item.get("phase") is None # no new phase
99
+ ):
93
100
  content_items[-1]["text"] += item["text"]
94
- if "signature" in item: # signature may appear at the last item
101
+ if "signature" in item: # finish the current item if signature is not None
95
102
  content_items[-1]["signature"] = item["signature"]
96
- elif item["text"]: # omit empty text items
103
+ elif item["text"] or item.get("phase") is not None: # text or new phase starts an item
97
104
  content_items.append(item.copy())
98
105
  elif item["type"] == "thinking":
99
- if content_items and content_items[-1]["type"] == "thinking":
106
+ if (
107
+ content_items
108
+ and content_items[-1]["type"] == "thinking"
109
+ and content_items[-1].get("signature") is None # no signature yet
110
+ ):
100
111
  content_items[-1]["thinking"] += item["thinking"]
101
- if "signature" in item: # signature may appear at the last item
112
+ if "signature" in item: # finish the current item if signature is not None
102
113
  content_items[-1]["signature"] = item["signature"]
103
114
  elif item["thinking"] or item.get("signature"): # omit empty thinking items
104
115
  content_items.append(item.copy())
@@ -110,15 +121,37 @@ class LLMClient(ABC):
110
121
 
111
122
  usage_metadata = event.get("usage_metadata") # usage_metadata is taken from the last event
112
123
  finish_reason = event.get("finish_reason") # finish_reason is taken from the last event
124
+ created_at = event.get("created_at") # created_at is taken from the last event
113
125
 
114
126
  return {
115
127
  "role": "assistant",
116
128
  "content_items": content_items,
117
129
  "usage_metadata": usage_metadata,
118
130
  "finish_reason": finish_reason,
131
+ "created_at": created_at,
119
132
  }
120
133
 
121
134
  @abstractmethod
135
+ async def _streaming_response_internal(
136
+ self,
137
+ messages: list[UniMessage],
138
+ config: UniConfig,
139
+ ) -> AsyncIterator[UniEvent]:
140
+ """
141
+ Internal method to handle streaming response.
142
+
143
+ This method should be implemented by each model client to handle
144
+ the actual streaming request and yield model-specific events.
145
+
146
+ Args:
147
+ messages: List of universal message dictionaries
148
+ config: Universal configuration dict
149
+
150
+ Yields:
151
+ Model-specific events from the streaming response
152
+ """
153
+ pass
154
+
122
155
  async def streaming_response(
123
156
  self,
124
157
  messages: list[UniMessage],
@@ -138,7 +171,28 @@ class LLMClient(ABC):
138
171
  Yields:
139
172
  Universal events from the streaming response
140
173
  """
141
- pass
174
+ # Stamp any messages that don't yet have a created_at timestamp
175
+ for msg in messages:
176
+ if "created_at" not in msg:
177
+ msg["created_at"] = int(time.time() * 1000)
178
+
179
+ last_event: UniEvent | None = None
180
+ events = []
181
+ async for event in self._streaming_response_internal(messages, config):
182
+ event["created_at"] = int(time.time() * 1000)
183
+ last_event = event
184
+ events.append(event)
185
+ yield event
186
+
187
+ self._validate_last_event(last_event)
188
+
189
+ # Save history to file if trace_id is specified
190
+ if config.get("trace_id") and events:
191
+ from .integration.tracer import Tracer
192
+
193
+ assistant_message = self.concat_uni_events_to_uni_message(events)
194
+ tracer = Tracer()
195
+ tracer.save_history(self._model, messages + [assistant_message], config["trace_id"], config)
142
196
 
143
197
  async def streaming_response_stateful(
144
198
  self,
@@ -159,26 +213,43 @@ class LLMClient(ABC):
159
213
  Yields:
160
214
  Universal events from the streaming response
161
215
  """
162
- # Add user message to history
163
- self._history.append(message)
216
+ # Build a temporary messages list for inference without mutating history yet
217
+ temp_messages = self._history + [message]
164
218
 
165
219
  # Collect all events for history
166
220
  events = []
167
- async for event in self.streaming_response(messages=self._history, config=config):
221
+ async for event in self.streaming_response(messages=temp_messages, config=config):
168
222
  events.append(event)
169
223
  yield event
170
224
 
171
- # Convert events to message and add to history
225
+ # Only update history after successful inference
226
+ # temp_messages[-1] is the user message, now stamped with created_at by streaming_response
172
227
  if events:
173
228
  assistant_message = self.concat_uni_events_to_uni_message(events)
229
+ self._history.append(temp_messages[-1])
174
230
  self._history.append(assistant_message)
175
231
 
176
- # Save history to file if trace_id is specified
177
- if config.get("trace_id"):
178
- from .integration.tracer import Tracer
232
+ @staticmethod
233
+ def _validate_last_event(last_event: UniEvent | None) -> None:
234
+ """Validate that the last event has usage_metadata and finish_reason.
179
235
 
180
- tracer = Tracer()
181
- tracer.save_history(self._model, self._history, config["trace_id"], config)
236
+ This validation guards against servers that silently terminate streaming
237
+ output partway through without sending a proper final event.
238
+
239
+ Args:
240
+ last_event: The last event yielded by streaming_response
241
+
242
+ Raises:
243
+ ValueError: If last_event is None or missing usage_metadata/finish_reason
244
+ """
245
+ if last_event is None:
246
+ raise ValueError("Streaming response yielded no events")
247
+
248
+ if last_event["usage_metadata"] is None:
249
+ raise ValueError(f"Last event must carry usage_metadata, got: {last_event}")
250
+
251
+ if last_event["finish_reason"] is None:
252
+ raise ValueError(f"Last event must carry finish_reason, got: {last_event}")
182
253
 
183
254
  def clear_history(self) -> None:
184
255
  """Clear the message history."""
@@ -187,3 +258,11 @@ class LLMClient(ABC):
187
258
  def get_history(self) -> list[UniMessage]:
188
259
  """Get the current message history."""
189
260
  return self._history.copy()
261
+
262
+ def set_history(self, history: list[UniMessage]) -> None:
263
+ """Replace the message history with a copy of the provided history.
264
+
265
+ Args:
266
+ history: List of universal message dictionaries to set as the new history
267
+ """
268
+ self._history = list(history)
@@ -12,7 +12,7 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from .client import Claude4_5Client
15
+ from .client import Claude4_6Client
16
16
 
17
17
 
18
- __all__ = ["Claude4_5Client"]
18
+ __all__ = ["Claude4_6Client"]