voxagent 0.2.3__py3-none-any.whl → 0.2.4__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.
voxagent/_version.py CHANGED
@@ -1,2 +1,2 @@
1
- __version__ = "0.2.3"
1
+ __version__ = "0.2.4"
2
2
  __version_info__ = tuple(int(x) for x in __version__.split("."))
voxagent/agent/core.py CHANGED
@@ -125,6 +125,68 @@ class Agent(Generic[DepsT, OutputT]):
125
125
  # Toolsets (MCP servers, etc.) - store for later
126
126
  self._toolsets = toolsets or []
127
127
 
128
+ # MCP connection caching (persistent across run() calls)
129
+ self._mcp_manager: MCPServerManager | None = None
130
+ self._mcp_tools: list[ToolDefinition] = []
131
+ self._mcp_connected: bool = False
132
+
133
+ # -------------------------------------------------------------------------
134
+ # MCP Connection Management
135
+ # -------------------------------------------------------------------------
136
+
137
+ async def connect_mcp(self) -> list[ToolDefinition]:
138
+ """Connect to MCP servers and cache the connection.
139
+
140
+ This method connects to all MCP servers in toolsets and caches the
141
+ connection for reuse across multiple run() calls. Call this during
142
+ initialization/warmup to avoid connection overhead on first message.
143
+
144
+ Returns:
145
+ List of ToolDefinition objects from connected MCP servers.
146
+ """
147
+ if self._mcp_connected:
148
+ return self._mcp_tools
149
+
150
+ if self._toolsets:
151
+ self._mcp_manager = MCPServerManager()
152
+ await self._mcp_manager.add_servers(self._toolsets)
153
+ self._mcp_tools = await self._mcp_manager.connect_all()
154
+ self._mcp_connected = True
155
+
156
+ return self._mcp_tools
157
+
158
+ async def disconnect_mcp(self) -> None:
159
+ """Disconnect from MCP servers.
160
+
161
+ Call this when the agent is no longer needed to clean up MCP
162
+ server connections. This is called automatically when using
163
+ the agent as an async context manager.
164
+ """
165
+ if self._mcp_manager and self._mcp_connected:
166
+ await self._mcp_manager.disconnect_all()
167
+ self._mcp_manager = None
168
+ self._mcp_connected = False
169
+ self._mcp_tools = []
170
+
171
+ @property
172
+ def mcp_connected(self) -> bool:
173
+ """Check if MCP servers are currently connected."""
174
+ return self._mcp_connected
175
+
176
+ async def __aenter__(self) -> "Agent[DepsT, OutputT]":
177
+ """Enter async context manager - connect MCP servers."""
178
+ await self.connect_mcp()
179
+ return self
180
+
181
+ async def __aexit__(
182
+ self,
183
+ exc_type: type[BaseException] | None,
184
+ exc_val: BaseException | None,
185
+ exc_tb: Any,
186
+ ) -> None:
187
+ """Exit async context manager - disconnect MCP servers."""
188
+ await self.disconnect_mcp()
189
+
128
190
  @staticmethod
129
191
  def _parse_model_string(model_string: str) -> ModelConfig:
130
192
  """Parse 'provider:model' string into ModelConfig.
@@ -495,19 +557,22 @@ class Agent(Generic[DepsT, OutputT]):
495
557
  timeout_handler: TimeoutHandler | None = None
496
558
  timed_out = False
497
559
  error_message: str | None = None
498
- mcp_manager: MCPServerManager | None = None
560
+ # Track if we connected MCP in this run (for cleanup)
561
+ mcp_connected_in_this_run = False
499
562
 
500
563
  if timeout_ms:
501
564
  timeout_handler = TimeoutHandler(timeout_ms)
502
565
  await timeout_handler.start(abort_controller)
503
566
 
504
567
  try:
505
- # Connect to MCP servers if any
506
- mcp_tools: list[ToolDefinition] = []
507
- if self._toolsets:
508
- mcp_manager = MCPServerManager()
509
- await mcp_manager.add_servers(self._toolsets)
510
- mcp_tools = await mcp_manager.connect_all()
568
+ # Use cached MCP connection if available, otherwise connect
569
+ if self._mcp_connected:
570
+ mcp_tools = self._mcp_tools
571
+ elif self._toolsets:
572
+ mcp_tools = await self.connect_mcp()
573
+ mcp_connected_in_this_run = True
574
+ else:
575
+ mcp_tools = []
511
576
 
512
577
  # Get all tools (native + MCP)
513
578
  all_tools = self._get_all_tools(mcp_tools)
@@ -652,9 +717,10 @@ class Agent(Generic[DepsT, OutputT]):
652
717
  )
653
718
 
654
719
  finally:
655
- # Disconnect MCP servers
656
- if mcp_manager:
657
- await mcp_manager.disconnect_all()
720
+ # Only disconnect MCP servers if we connected them in this run
721
+ # (not if using cached connection from connect_mcp())
722
+ if mcp_connected_in_this_run and not self._mcp_connected:
723
+ await self.disconnect_mcp()
658
724
  if timeout_handler:
659
725
  timeout_handler.cancel()
660
726
  abort_controller.cleanup()
@@ -685,19 +751,22 @@ class Agent(Generic[DepsT, OutputT]):
685
751
  abort_controller = AbortController()
686
752
  timeout_handler: TimeoutHandler | None = None
687
753
  timed_out = False
688
- mcp_manager: MCPServerManager | None = None
754
+ # Track if we connected MCP in this run (for cleanup)
755
+ mcp_connected_in_this_run = False
689
756
 
690
757
  if timeout_ms:
691
758
  timeout_handler = TimeoutHandler(timeout_ms)
692
759
  await timeout_handler.start(abort_controller)
693
760
 
694
761
  try:
695
- # Connect to MCP servers if any
696
- mcp_tools: list[ToolDefinition] = []
697
- if self._toolsets:
698
- mcp_manager = MCPServerManager()
699
- await mcp_manager.add_servers(self._toolsets)
700
- mcp_tools = await mcp_manager.connect_all()
762
+ # Use cached MCP connection if available, otherwise connect
763
+ if self._mcp_connected:
764
+ mcp_tools = self._mcp_tools
765
+ elif self._toolsets:
766
+ mcp_tools = await self.connect_mcp()
767
+ mcp_connected_in_this_run = True
768
+ else:
769
+ mcp_tools = []
701
770
 
702
771
  # Get all tools (native + MCP)
703
772
  all_tools = self._get_all_tools(mcp_tools)
@@ -838,9 +907,10 @@ class Agent(Generic[DepsT, OutputT]):
838
907
  )
839
908
 
840
909
  finally:
841
- # Disconnect MCP servers
842
- if mcp_manager:
843
- await mcp_manager.disconnect_all()
910
+ # Only disconnect MCP servers if we connected them in this run
911
+ # (not if using cached connection from connect_mcp())
912
+ if mcp_connected_in_this_run and not self._mcp_connected:
913
+ await self.disconnect_mcp()
844
914
  if timeout_handler:
845
915
  timeout_handler.cancel()
846
916
  abort_controller.cleanup()
@@ -206,7 +206,7 @@ class ChatGPTProvider(BaseProvider):
206
206
  input_msgs.append({
207
207
  "type": "message",
208
208
  "role": "assistant",
209
- "content": [{"type": "input_text", "text": msg.content}],
209
+ "content": [{"type": "output_text", "text": msg.content}],
210
210
  })
211
211
  # Add function_call items for each tool call
212
212
  for tc in msg.tool_calls:
@@ -242,25 +242,28 @@ class ChatGPTProvider(BaseProvider):
242
242
  "output": block.get("content", ""),
243
243
  })
244
244
  elif hasattr(block, "text"):
245
- # TextBlock
245
+ # TextBlock - use output_text for assistant, input_text for user
246
+ content_type = "output_text" if msg.role == "assistant" else "input_text"
246
247
  input_msgs.append({
247
248
  "type": "message",
248
249
  "role": "user" if msg.role == "user" else "assistant",
249
- "content": [{"type": "input_text", "text": block.text}],
250
+ "content": [{"type": content_type, "text": block.text}],
250
251
  })
251
252
  elif isinstance(block, dict) and "text" in block:
253
+ content_type = "output_text" if msg.role == "assistant" else "input_text"
252
254
  input_msgs.append({
253
255
  "type": "message",
254
256
  "role": "user" if msg.role == "user" else "assistant",
255
- "content": [{"type": "input_text", "text": block["text"]}],
257
+ "content": [{"type": content_type, "text": block["text"]}],
256
258
  })
257
259
  elif isinstance(msg.content, str):
258
- # Simple string content
260
+ # Simple string content - use output_text for assistant, input_text for user
259
261
  role = "user" if msg.role == "user" else "assistant"
262
+ content_type = "output_text" if msg.role == "assistant" else "input_text"
260
263
  input_msgs.append({
261
264
  "type": "message",
262
265
  "role": role,
263
- "content": [{"type": "input_text", "text": msg.content}],
266
+ "content": [{"type": content_type, "text": msg.content}],
264
267
  })
265
268
 
266
269
  return input_msgs
@@ -335,7 +338,13 @@ class ChatGPTProvider(BaseProvider):
335
338
  if response.status_code == 401:
336
339
  yield ErrorChunk(error="Authentication failed - token may be expired")
337
340
  return
338
- response.raise_for_status()
341
+ if response.status_code >= 400:
342
+ # Read error body before raising
343
+ error_body = await response.aread()
344
+ error_text = error_body.decode("utf-8", errors="replace")
345
+ logger.error("ChatGPT API error %d: %s", response.status_code, error_text)
346
+ yield ErrorChunk(error=f"HTTP {response.status_code}: {error_text[:500]}")
347
+ return
339
348
 
340
349
  async for line in response.aiter_lines():
341
350
  if abort_signal and abort_signal.aborted:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voxagent
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: A lightweight, model-agnostic LLM provider abstraction with streaming and tool support
5
5
  Project-URL: Homepage, https://github.com/lensator/voxagent
6
6
  Project-URL: Documentation, https://github.com/lensator/voxagent#readme
@@ -1,9 +1,9 @@
1
1
  voxagent/__init__.py,sha256=YMYC95iwWXK26hicGYmd2erNOInrYtohwUVOuNmpTCs,3927
2
- voxagent/_version.py,sha256=avoaFz2ddynhpdQn-udgPxrhqWOcXWc2liIjyh_vgkI,87
2
+ voxagent/_version.py,sha256=uG0jH_KsTBSDgsjnJdMe2gyLmoUNsNl07RdN-sUtP8Y,87
3
3
  voxagent/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
4
  voxagent/agent/__init__.py,sha256=eASoU7Zhvw8BtJ-iUqVN06S4fMLkHwDgUZbHeH2AUOM,755
5
5
  voxagent/agent/abort.py,sha256=2Wnnxq8Dcn7wQkKPHrba2o0OeOdzF4NNsl-usgE4CJw,5191
6
- voxagent/agent/core.py,sha256=zXDUubx6oWQCj1V997s2zJ0Xj0XCaWIPZNinTNMz2zY,30581
6
+ voxagent/agent/core.py,sha256=ddjDoWcDqiacRtNzef5wORR2f5uHqL1Y1gOmzoeXRFY,33339
7
7
  voxagent/code/__init__.py,sha256=MzbrYReislAB-lCZEZn_lBEPGjYZyPK-c9RFCq1nSm0,1379
8
8
  voxagent/code/agent.py,sha256=fVaOlJNvnHJedPCCW5Rmm4_-YRvgcYpaLicXA-8HWxU,9634
9
9
  voxagent/code/sandbox.py,sha256=LP2cwXchDk6mtiYGRb_RmkGNoyPv5OEKQC2h4M89dC0,11669
@@ -17,7 +17,7 @@ voxagent/providers/anthropic.py,sha256=A9aJUBc2nwqFdtvh5CyHp0GSQR82DaxwJQsgA79cf
17
17
  voxagent/providers/augment.py,sha256=K8ORZsOh88FBh4d7pYZUgMQHtQXnhJizpjwGkbWf53Y,8623
18
18
  voxagent/providers/auth.py,sha256=X3yDSHzj7gP-XxiYVpTSpDZs608gxnd-0UYX5aNNsEw,3650
19
19
  voxagent/providers/base.py,sha256=f_tPz4LFWX2nU9q1aa__Z3UpT35VpQnDVr40Z3VM6uk,6933
20
- voxagent/providers/chatgpt.py,sha256=DBjoc8UTONxwz2BGhsPVevSJBlLduai4gjzt62_lZrE,15620
20
+ voxagent/providers/chatgpt.py,sha256=YW7VVXiPyqmeWKwbXraRfKakYCdb0tlTfqB6RqRK0hY,16445
21
21
  voxagent/providers/claudecode.py,sha256=4pUnWO9IJlXnagnqu3bl6SIDdyjpDohjOwJqRe96g6U,4331
22
22
  voxagent/providers/cli_base.py,sha256=0N4psjsH9ekgfq6ynspPQ48iB5BmN6qiOSDPMHjJdBA,7554
23
23
  voxagent/providers/codex.py,sha256=NUtCH1fqf1OTI2JEOlNClofm6spWq4ZvlaPaHbyotyE,5397
@@ -52,6 +52,6 @@ voxagent/tools/registry.py,sha256=MNJzgcmKT0AoMWIky9TJY4WVhzn5dkmjIHsUiZ3mv3U,25
52
52
  voxagent/types/__init__.py,sha256=3VunuprKKEpOR9Cg-UITHJXds_xQ-tfqQb4S7wD3nP4,933
53
53
  voxagent/types/messages.py,sha256=c6hNi9w6C8gbFoFm5fFge35vwJGywaoR_OiPQprfyVs,3494
54
54
  voxagent/types/run.py,sha256=4vYq0pCqH7_7SWbMb1SplWj4TLiE3DELDYMi0HefFmo,5071
55
- voxagent-0.2.3.dist-info/METADATA,sha256=lxBJ9wIDJhCCLGb4P_ps9C0d5kfYVdTAHZNyDer9vYU,5685
56
- voxagent-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
57
- voxagent-0.2.3.dist-info/RECORD,,
55
+ voxagent-0.2.4.dist-info/METADATA,sha256=MwN72uka42N7nHRbI6F6d3YvqFklZmcAEDDyg093e04,5685
56
+ voxagent-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
57
+ voxagent-0.2.4.dist-info/RECORD,,