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.
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/CHANGELOG.md +27 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/PKG-INFO +1 -1
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/ChatModel.py +284 -3
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Media.py +65 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/PKG-INFO +1 -1
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/pyproject.toml +1 -1
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/AGENTX.md +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/CONTRIBUTING.md +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/LICENSE +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/MANIFEST.in +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/README.md +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Agents/Agent.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Agents/__init__.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/AsyncTools.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/AutoSetup.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Cache.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Compiler.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Config.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/DefaultTools.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Embeddings.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Evals.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Handoffs.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Loader.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/MCP.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Memory.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Observability.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Planner.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Runner/AgentRun.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Runner/AsyncAgentRun.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Runner/__init__.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Runner/promptTemplate.yaml +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Session.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Splitters.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Streaming.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Supervisor.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/Tools.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/VectorStores/__init__.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/VectorStores/chroma_store.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/VectorStores/pg_store.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/VectorStores/qdrant_store.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/WebTools.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/__init__.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/resources/__init__.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev/resources/promptTemplate.yaml +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/SOURCES.txt +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/dependency_links.txt +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/requires.txt +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/agentx_dev.egg-info/top_level.txt +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/README.md +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/agentic_rag_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/async_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/async_quickstart.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/auto_features_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/caching_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/chatbot_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/complete_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/concurrent_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/concurrent_tool_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/file_agent_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/function_calling_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/mcp_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/mcp_github_triage_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/observability_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/orchestration_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/planner_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/robust_link_scraper.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/supervisor_codebase_analysis_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/supervisor_example.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/sync_quickstart.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/v3_1_1_features_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/v3_1_comprehensive_demo.py +0 -0
- {agentx_dev-3.4.0 → agentx_dev-3.4.1}/examples/v3_1_features_demo.py +0 -0
- {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.
|
|
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
|
|
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=
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|