agentx-dev 3.4.0__tar.gz → 3.4.1__tar.gz

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.
Files changed (73) hide show
  1. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/CHANGELOG.md +27 -0
  2. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/PKG-INFO +1 -1
  3. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/ChatModel.py +284 -3
  4. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Media.py +65 -0
  5. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/PKG-INFO +1 -1
  6. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/pyproject.toml +1 -1
  7. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/AGENTX.md +0 -0
  8. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/CONTRIBUTING.md +0 -0
  9. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/LICENSE +0 -0
  10. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/MANIFEST.in +0 -0
  11. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/README.md +0 -0
  12. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Agents/Agent.py +0 -0
  13. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Agents/__init__.py +0 -0
  14. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/AsyncTools.py +0 -0
  15. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/AutoSetup.py +0 -0
  16. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Cache.py +0 -0
  17. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Compiler.py +0 -0
  18. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Config.py +0 -0
  19. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/DefaultTools.py +0 -0
  20. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Embeddings.py +0 -0
  21. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Evals.py +0 -0
  22. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Handoffs.py +0 -0
  23. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Loader.py +0 -0
  24. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/MCP.py +0 -0
  25. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Memory.py +0 -0
  26. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Observability.py +0 -0
  27. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Planner.py +0 -0
  28. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Runner/AgentRun.py +0 -0
  29. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Runner/AsyncAgentRun.py +0 -0
  30. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Runner/__init__.py +0 -0
  31. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Runner/promptTemplate.yaml +0 -0
  32. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Session.py +0 -0
  33. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Splitters.py +0 -0
  34. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Streaming.py +0 -0
  35. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Supervisor.py +0 -0
  36. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Tools.py +0 -0
  37. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/VectorStores/__init__.py +0 -0
  38. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/VectorStores/chroma_store.py +0 -0
  39. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/VectorStores/pg_store.py +0 -0
  40. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/VectorStores/qdrant_store.py +0 -0
  41. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/WebTools.py +0 -0
  42. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/__init__.py +0 -0
  43. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/resources/__init__.py +0 -0
  44. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/resources/promptTemplate.yaml +0 -0
  45. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/SOURCES.txt +0 -0
  46. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/dependency_links.txt +0 -0
  47. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/requires.txt +0 -0
  48. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/top_level.txt +0 -0
  49. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/README.md +0 -0
  50. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/agentic_rag_demo.py +0 -0
  51. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/async_example.py +0 -0
  52. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/async_quickstart.py +0 -0
  53. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/auto_features_example.py +0 -0
  54. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/caching_example.py +0 -0
  55. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/chatbot_example.py +0 -0
  56. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/complete_example.py +0 -0
  57. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/concurrent_example.py +0 -0
  58. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/concurrent_tool_example.py +0 -0
  59. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/file_agent_demo.py +0 -0
  60. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/function_calling_demo.py +0 -0
  61. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/mcp_demo.py +0 -0
  62. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/mcp_github_triage_demo.py +0 -0
  63. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/observability_example.py +0 -0
  64. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/orchestration_demo.py +0 -0
  65. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/planner_example.py +0 -0
  66. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/robust_link_scraper.py +0 -0
  67. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/supervisor_codebase_analysis_demo.py +0 -0
  68. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/supervisor_example.py +0 -0
  69. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/sync_quickstart.py +0 -0
  70. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/v3_1_1_features_demo.py +0 -0
  71. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/v3_1_comprehensive_demo.py +0 -0
  72. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/v3_1_features_demo.py +0 -0
  73. {agentx_dev-3.4.0 → agentx_dev-3.4.1}/setup.cfg +0 -0
@@ -4,6 +4,33 @@ All notable changes to `agentx-dev` are documented here. Format loosely
4
4
  follows [Keep a Changelog](https://keepachangelog.com/); versioning is
5
5
  [Semver](https://semver.org/).
6
6
 
7
+ ## [3.4.1] - 2026-09-18
8
+
9
+ Tool calls on OpenAI models that require the Responses API.
10
+
11
+ ### Fixed
12
+
13
+ - **Tool calls failed on models that need the Responses API.** Some
14
+ OpenAI models (e.g. `gpt-6-astra`) reject function tools combined with
15
+ reasoning on `/v1/chat/completions`, and the error's suggested fix --
16
+ `reasoning_effort='none'` -- is itself rejected by those models, so
17
+ 3.4.0's parameter adaptation had nowhere to go. `GPT` now switches a
18
+ model's tool calls to `/v1/responses` when the provider says to,
19
+ retries, and remembers it (WARNING-logged). History is translated only
20
+ at the wire (`function_call` / `function_call_output` items paired by
21
+ `call_id`), so agents, `tool_calls`, and completions are unchanged.
22
+ Learned parameter fixes carry across endpoints (`reasoning.effort` maps
23
+ to `reasoning_effort`). Requests are sent with `store=False`, matching
24
+ chat completions. Plain text calls and streaming stay on chat
25
+ completions.
26
+ - Requests chat completions can't express (a document URL) go to the
27
+ Responses API, which accepts `file_url`, instead of raising.
28
+
29
+ ### Added
30
+
31
+ - `GPT(use_responses_api=None | True | False)`: automatic (default),
32
+ always, or never.
33
+
7
34
  ## [3.4.0] - 2026-09-18
8
35
 
9
36
  Media input for GPT and Claude, models that adapt to each generation's
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-dev
3
- Version: 3.4.0
3
+ Version: 3.4.1
4
4
  Summary: A production-grade Python framework for building LLM agents. Multi-provider chat, permission-gated tools, RAG (in-mem + Chroma/Qdrant/pgvector), semantic memory, Supervisor + Handoffs multi-agent orchestration with streaming, evals harness, prompt caching, Batch API, prompt-optimizer, MCP integration.
5
5
  Author-email: Bruce-Arhin Shadrach <brucearhin098@gmail.com>
6
6
  License: MIT License
@@ -6,7 +6,9 @@ from openai.types.chat.completion_create_params import (
6
6
  )
7
7
  from openai._types import NOT_GIVEN, NotGiven
8
8
  from pydantic import BaseModel
9
- from agentx_dev.Media import content_for_openai, content_for_anthropic
9
+ from agentx_dev.Media import (
10
+ content_for_openai, content_for_anthropic, content_for_openai_responses,
11
+ )
10
12
  import asyncio
11
13
  import json
12
14
  import re
@@ -432,6 +434,11 @@ def _openai_param_fix(exc: Exception, kwargs: Dict[str, Any]):
432
434
  or re.search(r"Unrecognized request argument supplied: (\w+)", text))
433
435
  if m:
434
436
  param = m.group(1)
437
+ # The Responses API nests it as reasoning.effort; the framework keeps it
438
+ # flat as reasoning_effort on both endpoints, so one learned fix
439
+ # covers both.
440
+ if param and param.startswith("reasoning"):
441
+ param = "reasoning_effort"
435
442
  if not param or param in _PROTECTED_PARAMS or not _is_set(kwargs, param):
436
443
  return None
437
444
 
@@ -577,6 +584,135 @@ def _system_text(content: Any) -> str:
577
584
  return str(content or "")
578
585
 
579
586
 
587
+ # ----------------------------------------------------------------------
588
+ # OpenAI Responses API (/v1/responses)
589
+ # ----------------------------------------------------------------------
590
+ # Some OpenAI models refuse function tools combined with reasoning on
591
+ # /v1/chat/completions and point at /v1/responses instead, e.g.
592
+ # "Function tools with reasoning_effort are not supported for gpt-6-astra
593
+ # in /v1/chat/completions. To use function tools, use /v1/responses or
594
+ # set reasoning_effort to 'none'."
595
+ # When the model also rejects 'none', the Responses API is the ONLY way to
596
+ # call tools on it. GPT therefore switches a model's tool calls to the
597
+ # Responses API when the provider says to, remembers the switch, and keeps
598
+ # the rest of the framework's chat-completions-shaped history unchanged --
599
+ # the translation happens only at the wire.
600
+
601
+ # Parameters that exist on responses.create; anything else from the chat
602
+ # defaults (seed, stop, n, penalties, logit_bias, ...) has no Responses
603
+ # equivalent and is left out, with one WARNING.
604
+ _RESPONSES_PARAMS = frozenset({
605
+ "model", "input", "instructions", "tools", "tool_choice", "reasoning",
606
+ "max_output_tokens", "temperature", "top_p", "parallel_tool_calls",
607
+ "store", "metadata", "user", "service_tier", "top_logprobs",
608
+ "extra_headers", "extra_query", "extra_body", "timeout",
609
+ })
610
+
611
+
612
+ def _wants_responses_api(exc: Exception) -> bool:
613
+ """True when OpenAI says this request must go through /v1/responses."""
614
+ return getattr(exc, "status_code", None) == 400 and "/v1/responses" in _error_text(exc)
615
+
616
+
617
+ def _normalize_tool_spec_for_openai_responses(spec: Dict[str, Any]) -> Dict[str, Any]:
618
+ """Chat-completions or generic tool spec -> Responses function tool.
619
+
620
+ Responses tools are flat (no nested ``function`` key). ``strict`` is
621
+ False because framework schemas aren't guaranteed to meet strict
622
+ mode's requirements (every property required, no extra keys)."""
623
+ if spec.get("type") == "function" and "function" in spec:
624
+ fn = spec["function"]
625
+ name, desc, params = fn["name"], fn.get("description", ""), fn.get("parameters", {})
626
+ elif spec.get("type") == "function" and "name" in spec and "function" not in spec:
627
+ return spec # already Responses-shaped
628
+ else:
629
+ name = spec["name"]
630
+ desc = spec.get("description", "")
631
+ params = spec.get("parameters") or spec.get("input_schema") or {}
632
+ return {"type": "function", "name": name, "description": desc,
633
+ "parameters": params or {"type": "object", "properties": {}},
634
+ "strict": False}
635
+
636
+
637
+ def _messages_for_openai_responses(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
638
+ """Framework (chat-completions-shaped) history -> Responses ``input`` items.
639
+
640
+ - assistant turns with ``tool_calls`` -> ``function_call`` items
641
+ - ``role="tool"`` results -> ``function_call_output`` items
642
+ - everything else -> role/content messages, media
643
+ rendered as input_* parts
644
+
645
+ ``function_call`` items are sent WITHOUT an ``id``: an item that carries
646
+ the server-side ``fc_`` id must be accompanied by its reasoning item,
647
+ which a stateless client doesn't keep. ``call_id`` alone pairs each
648
+ call with its output."""
649
+ items: List[Dict[str, Any]] = []
650
+ for m in messages:
651
+ if not isinstance(m, dict):
652
+ continue
653
+ role = m.get("role")
654
+ content = m.get("content")
655
+ if role == "assistant" and m.get("tool_calls"):
656
+ text = _system_text(content) if isinstance(content, list) else (content or "")
657
+ if text:
658
+ items.append({"role": "assistant", "content": text})
659
+ for call in m["tool_calls"]:
660
+ fn = call.get("function", {})
661
+ items.append({
662
+ "type": "function_call",
663
+ "call_id": call.get("id") or "call_unknown",
664
+ "name": fn.get("name", ""),
665
+ "arguments": fn.get("arguments") or "{}",
666
+ })
667
+ continue
668
+ if role in ("tool", "function") and m.get("tool_call_id"):
669
+ items.append({"type": "function_call_output",
670
+ "call_id": m["tool_call_id"],
671
+ "output": content if isinstance(content, str) else json.dumps(content, default=str)})
672
+ continue
673
+ if role not in ("system", "developer", "user", "assistant"):
674
+ # Legacy role="function" without an id: an observation for the model.
675
+ role = "user"
676
+ if role == "assistant":
677
+ # Assistant history is plain text on this endpoint.
678
+ text = _system_text(content) if isinstance(content, list) else (content or "")
679
+ if not text:
680
+ continue
681
+ items.append({"role": "assistant", "content": text})
682
+ continue
683
+ if content is None or content == "":
684
+ continue
685
+ items.append({"role": role, "content": content_for_openai_responses(content)})
686
+ return items
687
+
688
+
689
+ def _parse_responses_output(response: Any) -> Dict[str, Any]:
690
+ """Responses output items -> the framework's call_with_tools result."""
691
+ tool_calls = []
692
+ texts = []
693
+ for item in getattr(response, "output", None) or []:
694
+ itype = getattr(item, "type", None)
695
+ if itype == "function_call":
696
+ raw = getattr(item, "arguments", "") or "{}"
697
+ args, arg_error = _parse_tool_arguments(raw)
698
+ call_id = getattr(item, "call_id", None) or getattr(item, "id", None)
699
+ if arg_error is not None:
700
+ logger.error(f"Model returned non-JSON tool arguments for "
701
+ f"'{getattr(item, 'name', '')}': {raw!r}")
702
+ return {"type": "invalid_tool_args", "name": getattr(item, "name", ""),
703
+ "id": call_id, "raw": raw, "error": arg_error}
704
+ tool_calls.append({"name": getattr(item, "name", ""), "input": args, "id": call_id})
705
+ elif itype == "message":
706
+ for part in getattr(item, "content", None) or []:
707
+ if getattr(part, "type", None) == "output_text":
708
+ texts.append(getattr(part, "text", "") or "")
709
+ if tool_calls:
710
+ first = tool_calls[0]
711
+ return {"type": "tool_use", "name": first["name"], "input": first["input"],
712
+ "id": first["id"], "tool_calls": tool_calls}
713
+ return {"type": "text", "text": "".join(texts)}
714
+
715
+
580
716
  def _normalize_tool_spec_for_openai(spec: Dict[str, Any]) -> Dict[str, Any]:
581
717
  """Accept either a generic ``to_tool_spec`` dict or a pre-built OpenAI tool dict."""
582
718
  if spec.get("type") == "function" and "function" in spec:
@@ -1117,6 +1253,7 @@ class GPT(BaseChatModel):
1117
1253
  parallel_tool_calls: bool | NotGiven = NOT_GIVEN,
1118
1254
  reasoning_effort: str | NotGiven = NOT_GIVEN,
1119
1255
  adapt_params: bool = True,
1256
+ use_responses_api: Optional[bool] = None,
1120
1257
  ):
1121
1258
  """
1122
1259
  ``reasoning_effort`` accepts any value a model generation uses
@@ -1131,6 +1268,15 @@ class GPT(BaseChatModel):
1131
1268
  the default), make the smallest change the error asks for,
1132
1269
  retry, and remember it for this model. Logged at WARNING. Set
1133
1270
  False to surface the raw provider error instead.
1271
+
1272
+ ``use_responses_api`` (default None = automatic): some models
1273
+ refuse function tools with reasoning on /v1/chat/completions and
1274
+ name /v1/responses as the fix. Automatic mode switches that
1275
+ model's tool calls to the Responses API when the provider says
1276
+ so, retries, and remembers it (logged at WARNING). True routes
1277
+ every non-streaming call through the Responses API; False never
1278
+ does and surfaces the provider error instead. Token streaming
1279
+ (``stream_text``) always uses chat completions.
1134
1280
  """
1135
1281
  self.api_key = api_key or os.getenv("OPENAI_API_KEY")
1136
1282
  self.client = OpenAI(
@@ -1176,6 +1322,10 @@ class GPT(BaseChatModel):
1176
1322
 
1177
1323
  self.timeout = timeout
1178
1324
  self._params = _ParamAdapter("OpenAI", _openai_param_fix, adapt_params)
1325
+ self.use_responses_api = use_responses_api
1326
+ # Models the provider told us need /v1/responses for tool calls.
1327
+ self._responses_models: set = set()
1328
+ self._warned_dropped: set = set()
1179
1329
 
1180
1330
  def _request_kwargs(self, exclude: Iterable[str] = ()) -> Dict[str, Any]:
1181
1331
  """Defaults for one request, minus ``exclude``, with known
@@ -1191,6 +1341,106 @@ class GPT(BaseChatModel):
1191
1341
  def _create(self, **kwargs):
1192
1342
  return self._params.call(self.client.chat.completions.create, kwargs)
1193
1343
 
1344
+ def _responses_create_flat(self, **kw):
1345
+ """Call responses.create from FLAT framework kwargs.
1346
+
1347
+ Parameter adaptation runs on the flat names (``reasoning_effort``,
1348
+ ``max_tokens``), so a fix learned on either endpoint applies to
1349
+ both; the nesting / renaming for the Responses API happens here,
1350
+ at the wire."""
1351
+ kw = dict(kw)
1352
+ effort = kw.pop("reasoning_effort", NOT_GIVEN)
1353
+ if effort is not NOT_GIVEN and effort is not None:
1354
+ kw["reasoning"] = {"effort": effort}
1355
+ tokens = NOT_GIVEN
1356
+ for name in ("max_completion_tokens", "max_tokens"):
1357
+ value = kw.pop(name, NOT_GIVEN)
1358
+ if tokens is NOT_GIVEN and value is not NOT_GIVEN and value is not None:
1359
+ tokens = value
1360
+ if tokens is not NOT_GIVEN:
1361
+ kw["max_output_tokens"] = tokens
1362
+ dropped = sorted(k for k, v in kw.items()
1363
+ if k not in _RESPONSES_PARAMS and v is not NOT_GIVEN and v is not None)
1364
+ for k in list(kw):
1365
+ if k not in _RESPONSES_PARAMS:
1366
+ kw.pop(k)
1367
+ new_drops = [k for k in dropped if k not in self._warned_dropped]
1368
+ if new_drops:
1369
+ self._warned_dropped.update(new_drops)
1370
+ logger.warning(
1371
+ f"OpenAI Responses API has no equivalent for {new_drops}; "
1372
+ "those settings are not sent on this endpoint."
1373
+ )
1374
+ # Responses stores requests server-side by default; chat completions
1375
+ # does not. Keep the framework's behaviour the same on both.
1376
+ if kw.get("store", NOT_GIVEN) in (NOT_GIVEN, None):
1377
+ kw["store"] = False
1378
+ kw = {k: v for k, v in kw.items() if v is not NOT_GIVEN}
1379
+ return self.client.responses.create(**kw)
1380
+
1381
+ def _uses_responses(self, model: Any, *, for_tools: bool) -> bool:
1382
+ if self.use_responses_api is True:
1383
+ return True
1384
+ if self.use_responses_api is False:
1385
+ return False
1386
+ return for_tools and str(model) in self._responses_models
1387
+
1388
+ def _switch_to_responses(self, model: Any, exc: Exception) -> bool:
1389
+ """Record that ``model`` needs the Responses API for tool calls, if
1390
+ the provider said so and automatic mode is on."""
1391
+ if self.use_responses_api is not None or not _wants_responses_api(exc):
1392
+ return False
1393
+ self._responses_models.add(str(model))
1394
+ logger.warning(
1395
+ f"OpenAI model {str(model)!r} can't call tools on /v1/chat/completions "
1396
+ "with the current settings; switching its tool calls to the Responses "
1397
+ f"API (/v1/responses) and retrying. Remembered for this model. "
1398
+ f"(Provider said: {_error_text(exc)[:200]})"
1399
+ )
1400
+ return True
1401
+
1402
+ def _record_responses_usage(self, response: Any) -> None:
1403
+ u = getattr(response, "usage", None)
1404
+ if u is not None:
1405
+ self._record_usage_counts(
1406
+ input_tokens=getattr(u, "input_tokens", 0) or 0,
1407
+ output_tokens=getattr(u, "output_tokens", 0) or 0,
1408
+ )
1409
+
1410
+ def _call_with_tools_responses(self, messages, tools, force_tool):
1411
+ normalized = [_normalize_tool_spec_for_openai_responses(t) for t in tools]
1412
+ tool_choice: Any = {"type": "function", "name": force_tool} if force_tool else "auto"
1413
+ kwargs = self._request_kwargs(
1414
+ ("tools", "tool_choice", "functions", "function_call", "response_format")
1415
+ )
1416
+ kwargs.update(input=_messages_for_openai_responses(list(messages)),
1417
+ tools=normalized, tool_choice=tool_choice)
1418
+ try:
1419
+ response = self._with_retry(
1420
+ lambda: self._params.call(self._responses_create_flat, kwargs),
1421
+ max_retries=3, base_delay=0.1,
1422
+ )
1423
+ except Exception as e:
1424
+ logger.error(f"Error during Responses API call (tools): {e}")
1425
+ raise
1426
+ self._record_responses_usage(response)
1427
+ return _parse_responses_output(response)
1428
+
1429
+ def _initialize_responses(self, messages, extra_headers, extra_query, extra_body, timeout):
1430
+ kwargs = self._request_kwargs(
1431
+ ("tools", "tool_choice", "functions", "function_call", "response_format")
1432
+ )
1433
+ kwargs.update(input=_messages_for_openai_responses(list(messages)),
1434
+ extra_headers=extra_headers, extra_query=extra_query,
1435
+ extra_body=extra_body, timeout=timeout or self.timeout)
1436
+ response = self._with_retry(
1437
+ lambda: self._params.call(self._responses_create_flat, kwargs),
1438
+ max_retries=3, base_delay=0.1,
1439
+ )
1440
+ self._record_responses_usage(response)
1441
+ parsed = _parse_responses_output(response)
1442
+ return parsed.get("text", "") if parsed["type"] == "text" else ""
1443
+
1194
1444
  def Initialize(
1195
1445
  self,
1196
1446
  messages: Iterable[ChatCompletionMessageParam],
@@ -1200,11 +1450,26 @@ class GPT(BaseChatModel):
1200
1450
  extra_body: Optional[Dict[str, Any]] = None,
1201
1451
  timeout: Optional[float] = None,
1202
1452
  ):
1453
+ if self._uses_responses(self.defaults.get("model"), for_tools=False):
1454
+ try:
1455
+ return self._initialize_responses(messages, extra_headers, extra_query,
1456
+ extra_body, timeout)
1457
+ except Exception as e:
1458
+ logger.error(f"Error during Responses API call: {e}")
1459
+ raise
1203
1460
  logger.debug("Calling OpenAI chat.completions.create")
1461
+ try:
1462
+ chat_messages = _messages_for_openai(list(messages))
1463
+ except ValueError:
1464
+ # e.g. a document URL: chat completions can't express it.
1465
+ if self.use_responses_api is False:
1466
+ raise
1467
+ return self._initialize_responses(messages, extra_headers, extra_query,
1468
+ extra_body, timeout)
1204
1469
 
1205
1470
  def _call_and_record():
1206
1471
  completion = self._create(
1207
- messages=_messages_for_openai(list(messages)),
1472
+ messages=chat_messages,
1208
1473
  **self._request_kwargs(),
1209
1474
  extra_headers=extra_headers,
1210
1475
  extra_query=extra_query,
@@ -1234,6 +1499,10 @@ class GPT(BaseChatModel):
1234
1499
  *,
1235
1500
  force_tool: Optional[str] = None,
1236
1501
  ) -> Dict[str, Any]:
1502
+ model = self.defaults.get("model")
1503
+ if self._uses_responses(model, for_tools=True):
1504
+ return self._call_with_tools_responses(messages, tools, force_tool)
1505
+ original_messages = messages
1237
1506
  normalized = [_normalize_tool_spec_for_openai(t) for t in tools]
1238
1507
  if force_tool:
1239
1508
  tool_choice: Any = {"type": "function", "function": {"name": force_tool}}
@@ -1247,7 +1516,17 @@ class GPT(BaseChatModel):
1247
1516
 
1248
1517
  # #15: translate any tool_use / tool_call_id messages to OpenAI's
1249
1518
  # native shape. Plain {role, content} dicts pass through unchanged.
1250
- messages = _messages_for_openai(messages)
1519
+ # Some inputs have no chat-completions form at all (a document
1520
+ # URL); the Responses API can take them, so route there unless
1521
+ # the caller pinned chat completions.
1522
+ try:
1523
+ messages = _messages_for_openai(messages)
1524
+ except ValueError:
1525
+ if self.use_responses_api is False:
1526
+ raise
1527
+ logger.info("Request needs the Responses API (chat completions "
1528
+ "can't express it); sending via /v1/responses.")
1529
+ return self._call_with_tools_responses(original_messages, tools, force_tool)
1251
1530
 
1252
1531
  try:
1253
1532
  response = self._with_retry(
@@ -1261,6 +1540,8 @@ class GPT(BaseChatModel):
1261
1540
  base_delay=0.1,
1262
1541
  )
1263
1542
  except Exception as e:
1543
+ if self._switch_to_responses(model, e):
1544
+ return self._call_with_tools_responses(original_messages, tools, force_tool)
1264
1545
  logger.error(f"Error during chat completion (tools): {e}")
1265
1546
  raise
1266
1547
 
@@ -60,6 +60,7 @@ __all__ = [
60
60
  "user_content",
61
61
  "split_text_and_media",
62
62
  "content_for_openai",
63
+ "content_for_openai_responses",
63
64
  "content_for_anthropic",
64
65
  "is_media_part",
65
66
  ]
@@ -446,6 +447,70 @@ def content_for_openai(content: Any) -> Any:
446
447
  return out
447
448
 
448
449
 
450
+ def content_for_openai_responses(content: Any) -> Any:
451
+ """Translate message content to OpenAI **Responses API** input parts.
452
+
453
+ Same inputs as :func:`content_for_openai`, different wire shapes:
454
+ ``input_text`` / ``input_image`` / ``input_file`` / ``input_audio``.
455
+ Unlike chat completions, the Responses API can fetch a document from
456
+ a URL (``file_url``), so that combination is allowed here."""
457
+ if isinstance(content, Media):
458
+ content = [content]
459
+ if not isinstance(content, list):
460
+ return content
461
+ out: List[Any] = []
462
+ for part in content:
463
+ if isinstance(part, str):
464
+ out.append({"type": "input_text", "text": part})
465
+ continue
466
+ if isinstance(part, Media):
467
+ part = part.to_part()
468
+ if not isinstance(part, dict):
469
+ out.append(part)
470
+ continue
471
+ ptype = part.get("type")
472
+ if ptype in ("text", "input_text"):
473
+ out.append({"type": "input_text", "text": str(part.get("text", ""))})
474
+ continue
475
+ if ptype in ("input_image", "input_file", "input_audio"):
476
+ out.append(part) # already Responses-native
477
+ continue
478
+ part = _canonicalise_part(part)
479
+ ptype = part.get("type")
480
+ if ptype == "file":
481
+ spec = part.get("file") or {}
482
+ if spec.get("file_id"):
483
+ out.append({"type": "input_file", "file_id": spec["file_id"]})
484
+ continue
485
+ if ptype not in _MEDIA_TYPES:
486
+ out.append(part)
487
+ continue
488
+ source = part.get("source") or {}
489
+ is_url = source.get("type") == "url"
490
+ if ptype == "image":
491
+ out.append({"type": "input_image",
492
+ "image_url": source.get("url") if is_url else _data_uri(source),
493
+ "detail": part.get("detail") or "auto"})
494
+ elif ptype == "document":
495
+ if is_url:
496
+ out.append({"type": "input_file", "file_url": source.get("url")})
497
+ else:
498
+ out.append({"type": "input_file",
499
+ "filename": part.get("filename") or "document.pdf",
500
+ "file_data": _data_uri(source)})
501
+ elif ptype == "audio":
502
+ if is_url:
503
+ raise ValueError("OpenAI audio input must be inline; use Media.audio(path_or_bytes).")
504
+ fmt = _AUDIO_FORMAT.get((source.get("media_type") or "").lower())
505
+ if fmt is None:
506
+ raise ValueError(
507
+ f"OpenAI audio input supports wav and mp3 only; got {source.get('media_type')!r}"
508
+ )
509
+ out.append({"type": "input_audio",
510
+ "input_audio": {"data": source.get("data", ""), "format": fmt}})
511
+ return out
512
+
513
+
449
514
  def content_for_anthropic(content: Any) -> Any:
450
515
  """Translate message content to Anthropic content blocks.
451
516
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentx-dev
3
- Version: 3.4.0
3
+ Version: 3.4.1
4
4
  Summary: A production-grade Python framework for building LLM agents. Multi-provider chat, permission-gated tools, RAG (in-mem + Chroma/Qdrant/pgvector), semantic memory, Supervisor + Handoffs multi-agent orchestration with streaming, evals harness, prompt caching, Batch API, prompt-optimizer, MCP integration.
5
5
  Author-email: Bruce-Arhin Shadrach <brucearhin098@gmail.com>
6
6
  License: MIT License
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
 
3
3
  name = "agentx-dev"
4
- version = "3.4.0"
4
+ version = "3.4.1"
5
5
  description = "A production-grade Python framework for building LLM agents. Multi-provider chat, permission-gated tools, RAG (in-mem + Chroma/Qdrant/pgvector), semantic memory, Supervisor + Handoffs multi-agent orchestration with streaming, evals harness, prompt caching, Batch API, prompt-optimizer, MCP integration."
6
6
  authors = [{ name = "Bruce-Arhin Shadrach", email = "brucearhin098@gmail.com" }]
7
7
  # Best practice: link to the license file
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes