pyagentic-core 2.8.1.dev1__tar.gz → 2.9.0a1__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.
- {pyagentic_core-2.8.1.dev1/pyagentic_core.egg-info → pyagentic_core-2.9.0a1}/PKG-INFO +1 -1
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/_app.py +158 -48
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1/pyagentic_core.egg-info}/PKG-INFO +1 -1
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/test_app.py +21 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/test_router.py +63 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/.github/workflows/docs.yml +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/.github/workflows/release.yml +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/.github/workflows/testing.yml +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/.gitignore +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/CHANGELOG.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/LICENSE +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/README.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/agent-linking.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/api/creating-an-app.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/api/deploying.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/api/index.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/api/jobs.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/api/running.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/diagrams/declaration.svg +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/diagrams/instantiation.svg +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/diagrams/runtime.svg +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/diagrams/source/README.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/diagrams/source/declaration.d2 +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/diagrams/source/instantiation.d2 +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/diagrams/source/runtime.d2 +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/execution-modes.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/getting-started.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/images/langfuse.png +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/index.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/inheritance.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/observability.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/phases.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/policies.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/reference/architecture.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/reference/modules.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/responses.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/states.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/structured-output.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/docs/tools.md +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/mkdocs.yml +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_agent/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_agent/_agent.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_agent/_agent_linking.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_agent/_agent_state.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_exceptions.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_info.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_mcp.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_metaclasses.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_ref.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_spec.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_state.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_tool.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_validation.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_utils/_typing.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_utils/_warnings.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_version_scheme.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/_config.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/_docker.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/_mcp_server.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/_sessions.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/_models.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/_orchestrator.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/_routes.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/backends/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/backends/_base.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/backends/_in_process.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/store/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/store/_base.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/store/_sqlite.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/llm/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/llm/_anthropic.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/llm/_gemini.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/llm/_mock.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/llm/_openai.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/llm/_openaiv1.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/llm/_provider.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/logging.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/models/llm.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/models/response.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/models/tracing.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/policies/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/policies/_events.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/policies/_policy.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/tracing/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/tracing/_basic.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/tracing/_langfuse.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/tracing/_tracer.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/updates.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic_core.egg-info/SOURCES.txt +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic_core.egg-info/dependency_links.txt +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic_core.egg-info/requires.txt +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic_core.egg-info/top_level.txt +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyproject.toml +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/setup.cfg +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/setup.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_agent.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_agent_inheritance.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_agent_linking.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_agent_provider.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_params.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_phases.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_state.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_tool.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/_base/test_validator.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/jobs/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/jobs/test_orchestrator.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/jobs/test_routes.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/jobs/test_store_sqlite.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/test_config.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/test_docker.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/test_mcp_server.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/test_metaclass_models.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/api/test_sessions.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/conftest.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/models/test_responses.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/tracing/__init__.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/tracing/test_basic_tracer.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/tracing/test_models.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/tests/tracing/test_tracer.py +0 -0
- {pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/uv.lock +0 -0
|
@@ -29,7 +29,7 @@ from pydantic import BaseModel
|
|
|
29
29
|
from pyagentic._base._agent._agent import BaseAgent
|
|
30
30
|
from pyagentic.models.llm import LLMResponse
|
|
31
31
|
from pyagentic.models.response import AgentResponse, ToolResponse
|
|
32
|
-
from pyagentic.api._config import AgentsConfig, load_config
|
|
32
|
+
from pyagentic.api._config import AgentsConfig, JobsConfig, load_config
|
|
33
33
|
from pyagentic.api._sessions import SessionManager
|
|
34
34
|
|
|
35
35
|
# Agents may be passed as a single class, a list of classes, or a
|
|
@@ -79,16 +79,21 @@ def _validate_agent_class(cls: type) -> None:
|
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
def _slugify(value: str) -> str:
|
|
83
|
+
"""Convert a CamelCase or arbitrary name into a kebab-case URL/route slug."""
|
|
84
|
+
# Insert a hyphen at lower/digit -> upper boundaries (CamelCase -> kebab).
|
|
85
|
+
slug = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "-", value).lower()
|
|
86
|
+
# Collapse any remaining non-alphanumeric runs into single hyphens.
|
|
87
|
+
slug = re.sub(r"[^a-z0-9]+", "-", slug).strip("-")
|
|
88
|
+
return slug or "agent"
|
|
89
|
+
|
|
90
|
+
|
|
82
91
|
def _default_prefix(agent_class: type[BaseAgent]) -> str:
|
|
83
92
|
"""Derive a URL prefix from an agent class name (e.g. ResearchAgent -> /research)."""
|
|
84
93
|
name = agent_class.__name__
|
|
85
94
|
if name.endswith("Agent") and len(name) > len("Agent"):
|
|
86
95
|
name = name[: -len("Agent")]
|
|
87
|
-
|
|
88
|
-
name = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "-", name).lower()
|
|
89
|
-
# Collapse any remaining non-alphanumeric runs into single hyphens.
|
|
90
|
-
name = re.sub(r"[^a-z0-9]+", "-", name).strip("-")
|
|
91
|
-
return "/" + name
|
|
96
|
+
return "/" + _slugify(name)
|
|
92
97
|
|
|
93
98
|
|
|
94
99
|
def _store_path_for(base: str, prefix: str, multi: bool) -> str:
|
|
@@ -135,33 +140,60 @@ def create_router(
|
|
|
135
140
|
model: Optional[str] = None,
|
|
136
141
|
name: Optional[str] = None,
|
|
137
142
|
version: str = "0.1.0",
|
|
143
|
+
tags: Optional[list[str]] = None,
|
|
144
|
+
sessions: bool = True,
|
|
145
|
+
jobs: bool = False,
|
|
146
|
+
jobs_config: Optional["JobsConfig"] = None,
|
|
138
147
|
) -> APIRouter:
|
|
139
148
|
"""Build a FastAPI ``APIRouter`` exposing a single agent's endpoints.
|
|
140
149
|
|
|
141
|
-
Mount it onto an existing FastAPI app::
|
|
150
|
+
Mount it onto an existing FastAPI app under whatever prefix you like::
|
|
142
151
|
|
|
143
|
-
app.include_router(create_router(MyAgent), prefix="/agent")
|
|
152
|
+
app.include_router(create_router(MyAgent, name="my-agent"), prefix="/agents/my-agent")
|
|
144
153
|
|
|
145
154
|
The router owns its own :class:`SessionManager`, exposed as
|
|
146
155
|
``router.sessions`` so callers can share it (e.g. with :func:`mount_mcp`).
|
|
156
|
+
When ``jobs`` is enabled, the job orchestrator is exposed as
|
|
157
|
+
``router.orchestrator`` (otherwise ``None``) so callers can wire its
|
|
158
|
+
lifecycle into their app's lifespan.
|
|
159
|
+
|
|
160
|
+
Every route is given a unique ``name`` derived from ``name`` (e.g.
|
|
161
|
+
``my-agent_chat``), so multiple agents can be mounted on one app without
|
|
162
|
+
colliding and individual routes remain addressable via ``url_path_for``.
|
|
147
163
|
|
|
148
164
|
Routes registered (relative to the mount point):
|
|
149
165
|
- ``GET /`` agent info, ``GET /health``, ``GET /schema``
|
|
150
|
-
- ``POST/GET /sessions``, ``DELETE /sessions/{id}
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
- when ``sessions``: ``POST/GET /sessions``, ``DELETE /sessions/{id}``,
|
|
167
|
+
``POST /sessions/{id}/chat``, ``POST /sessions/{id}/chat/stream``,
|
|
168
|
+
``GET /sessions/{id}/state``
|
|
169
|
+
- when ``jobs``: ``POST/GET /jobs``, ``GET /jobs/{id}``,
|
|
170
|
+
``GET /jobs/{id}/updates``, ``GET /jobs/{id}/stream``,
|
|
171
|
+
``POST /jobs/{id}/cancel``
|
|
153
172
|
|
|
154
173
|
Args:
|
|
155
174
|
agent_class (type[BaseAgent]): The agent class to serve.
|
|
156
175
|
model (Optional[str]): Default model for sessions. ``None`` falls back to
|
|
157
176
|
the agent's own default.
|
|
158
|
-
name (Optional[str]): Display name for the info route
|
|
159
|
-
class name.
|
|
177
|
+
name (Optional[str]): Display name for the info route and basis for
|
|
178
|
+
route names/tags. Defaults to the class name.
|
|
160
179
|
version (str): Version string for the info route.
|
|
180
|
+
tags (Optional[list[str]]): OpenAPI tags applied to every route on the
|
|
181
|
+
router (groups them in the docs). ``None`` leaves routes untagged.
|
|
182
|
+
sessions (bool): If True, mount the session-based routes (create/list/
|
|
183
|
+
delete sessions plus synchronous chat, streaming chat, and state).
|
|
184
|
+
If False, those routes are omitted — interaction happens only via
|
|
185
|
+
``jobs``.
|
|
186
|
+
jobs (bool): If True, mount a durable async job system at ``/jobs`` whose
|
|
187
|
+
orchestrator is exposed as ``router.orchestrator``. The caller is
|
|
188
|
+
responsible for starting/stopping that orchestrator (see
|
|
189
|
+
:func:`create_app`, which does this automatically).
|
|
190
|
+
jobs_config (Optional[JobsConfig]): Configuration for the job system when
|
|
191
|
+
``jobs`` is True. Defaults to :class:`JobsConfig` defaults.
|
|
161
192
|
|
|
162
193
|
Returns:
|
|
163
|
-
APIRouter: A router with
|
|
164
|
-
``SessionManager`` attached as ``router.sessions
|
|
194
|
+
APIRouter: A router with the selected agent routes registered, with its
|
|
195
|
+
``SessionManager`` attached as ``router.sessions`` and its job
|
|
196
|
+
orchestrator (or ``None``) as ``router.orchestrator``.
|
|
165
197
|
|
|
166
198
|
Raises:
|
|
167
199
|
TypeError: If agent_class is missing required metaclass attributes.
|
|
@@ -169,17 +201,18 @@ def create_router(
|
|
|
169
201
|
_validate_agent_class(agent_class)
|
|
170
202
|
|
|
171
203
|
name = name or agent_class.__name__
|
|
204
|
+
slug = _slugify(name)
|
|
172
205
|
RequestModel = agent_class.__request_model__
|
|
173
206
|
ResponseModel = agent_class.__response_model__
|
|
174
207
|
StreamEventModel = agent_class.__stream_event_model__
|
|
175
208
|
StateModel = agent_class.__state_class__
|
|
176
209
|
|
|
177
|
-
router = APIRouter()
|
|
178
|
-
|
|
210
|
+
router = APIRouter(tags=tags)
|
|
211
|
+
session_manager = SessionManager(agent_class, default_model=model)
|
|
179
212
|
|
|
180
213
|
# ---- info routes ----
|
|
181
214
|
|
|
182
|
-
@router.get("/")
|
|
215
|
+
@router.get("/", name=f"{slug}_info")
|
|
183
216
|
async def agent_info() -> dict:
|
|
184
217
|
"""Return basic agent metadata."""
|
|
185
218
|
return {
|
|
@@ -191,12 +224,12 @@ def create_router(
|
|
|
191
224
|
"linked_agents": list(agent_class.__linked_agents__.keys()),
|
|
192
225
|
}
|
|
193
226
|
|
|
194
|
-
@router.get("/health")
|
|
227
|
+
@router.get("/health", name=f"{slug}_health")
|
|
195
228
|
async def health() -> dict:
|
|
196
229
|
"""Liveness probe endpoint."""
|
|
197
230
|
return {"status": "ok"}
|
|
198
231
|
|
|
199
|
-
@router.get("/schema")
|
|
232
|
+
@router.get("/schema", name=f"{slug}_schema")
|
|
200
233
|
async def schema() -> dict:
|
|
201
234
|
"""Return JSON schemas for request, response, stream event, and state models."""
|
|
202
235
|
return {
|
|
@@ -206,9 +239,75 @@ def create_router(
|
|
|
206
239
|
"state": StateModel.model_json_schema(),
|
|
207
240
|
}
|
|
208
241
|
|
|
242
|
+
if sessions:
|
|
243
|
+
_register_session_routes(
|
|
244
|
+
router,
|
|
245
|
+
slug,
|
|
246
|
+
session_manager,
|
|
247
|
+
RequestModel,
|
|
248
|
+
ResponseModel,
|
|
249
|
+
StreamEventModel,
|
|
250
|
+
StateModel,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Expose the session manager so callers can share it (e.g. with mount_mcp).
|
|
254
|
+
router.sessions = session_manager
|
|
255
|
+
|
|
256
|
+
orchestrator = None
|
|
257
|
+
if jobs:
|
|
258
|
+
orchestrator = _mount_jobs_router(
|
|
259
|
+
router,
|
|
260
|
+
agent_class,
|
|
261
|
+
session_manager,
|
|
262
|
+
jobs_config or JobsConfig(),
|
|
263
|
+
model,
|
|
264
|
+
tags,
|
|
265
|
+
)
|
|
266
|
+
router.orchestrator = orchestrator
|
|
267
|
+
|
|
268
|
+
return router
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _mount_jobs_router(
|
|
272
|
+
router: APIRouter,
|
|
273
|
+
agent_class: type[BaseAgent],
|
|
274
|
+
sessions: SessionManager,
|
|
275
|
+
jobs_config: "JobsConfig",
|
|
276
|
+
default_model: Optional[str],
|
|
277
|
+
tags: Optional[list[str]],
|
|
278
|
+
) -> "JobOrchestrator":
|
|
279
|
+
"""Build a job orchestrator and include its /jobs router; return the orchestrator."""
|
|
280
|
+
from pyagentic.api.jobs import JobOrchestrator, build_backend, build_store
|
|
281
|
+
from pyagentic.api.jobs._routes import build_jobs_router
|
|
282
|
+
|
|
283
|
+
store = build_store(jobs_config.store)
|
|
284
|
+
backend = build_backend(
|
|
285
|
+
agent_class,
|
|
286
|
+
sessions,
|
|
287
|
+
max_concurrency=jobs_config.max_concurrency,
|
|
288
|
+
default_model=default_model,
|
|
289
|
+
)
|
|
290
|
+
orchestrator = JobOrchestrator(store, backend, jobs_config, sessions)
|
|
291
|
+
router.include_router(
|
|
292
|
+
build_jobs_router(orchestrator, agent_class, sessions), tags=tags
|
|
293
|
+
)
|
|
294
|
+
return orchestrator
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _register_session_routes(
|
|
298
|
+
router: APIRouter,
|
|
299
|
+
slug: str,
|
|
300
|
+
sessions: SessionManager,
|
|
301
|
+
RequestModel: type,
|
|
302
|
+
ResponseModel: type,
|
|
303
|
+
StreamEventModel: type,
|
|
304
|
+
StateModel: type,
|
|
305
|
+
) -> None:
|
|
306
|
+
"""Register the session-based routes (sessions CRUD, chat, stream, state)."""
|
|
307
|
+
|
|
209
308
|
# ---- session routes ----
|
|
210
309
|
|
|
211
|
-
@router.post("/sessions", status_code=201)
|
|
310
|
+
@router.post("/sessions", status_code=201, name=f"{slug}_create_session")
|
|
212
311
|
async def create_session(req: Optional[CreateSessionRequest] = None) -> dict:
|
|
213
312
|
"""Create a new agent session."""
|
|
214
313
|
model = req.model if req else None
|
|
@@ -216,12 +315,12 @@ def create_router(
|
|
|
216
315
|
session_id = sessions.create(model=model, api_key=api_key)
|
|
217
316
|
return {"session_id": session_id}
|
|
218
317
|
|
|
219
|
-
@router.get("/sessions")
|
|
318
|
+
@router.get("/sessions", name=f"{slug}_list_sessions")
|
|
220
319
|
async def list_sessions() -> dict:
|
|
221
320
|
"""List all active session IDs."""
|
|
222
321
|
return {"sessions": sessions.list_sessions()}
|
|
223
322
|
|
|
224
|
-
@router.delete("/sessions/{session_id}")
|
|
323
|
+
@router.delete("/sessions/{session_id}", name=f"{slug}_delete_session")
|
|
225
324
|
async def delete_session(session_id: str) -> dict:
|
|
226
325
|
"""Delete an existing session."""
|
|
227
326
|
try:
|
|
@@ -232,7 +331,11 @@ def create_router(
|
|
|
232
331
|
|
|
233
332
|
# ---- chat routes ----
|
|
234
333
|
|
|
235
|
-
@router.post(
|
|
334
|
+
@router.post(
|
|
335
|
+
"/sessions/{session_id}/chat",
|
|
336
|
+
response_model=ResponseModel,
|
|
337
|
+
name=f"{slug}_chat",
|
|
338
|
+
)
|
|
236
339
|
async def chat(session_id: str, req: RequestModel): # type: ignore[valid-type]
|
|
237
340
|
"""Send a message and receive a complete agent response."""
|
|
238
341
|
try:
|
|
@@ -247,6 +350,7 @@ def create_router(
|
|
|
247
350
|
@router.post(
|
|
248
351
|
"/sessions/{session_id}/chat/stream",
|
|
249
352
|
response_model=StreamEventModel,
|
|
353
|
+
name=f"{slug}_chat_stream",
|
|
250
354
|
responses={
|
|
251
355
|
200: {
|
|
252
356
|
"description": (
|
|
@@ -307,7 +411,11 @@ def create_router(
|
|
|
307
411
|
|
|
308
412
|
# ---- state route ----
|
|
309
413
|
|
|
310
|
-
@router.get(
|
|
414
|
+
@router.get(
|
|
415
|
+
"/sessions/{session_id}/state",
|
|
416
|
+
response_model=StateModel,
|
|
417
|
+
name=f"{slug}_get_state",
|
|
418
|
+
)
|
|
311
419
|
async def get_state(session_id: str):
|
|
312
420
|
"""Return the current state of the agent for a session."""
|
|
313
421
|
try:
|
|
@@ -316,11 +424,6 @@ def create_router(
|
|
|
316
424
|
raise HTTPException(status_code=404, detail="Session not found")
|
|
317
425
|
return agent.state
|
|
318
426
|
|
|
319
|
-
# Expose the session manager so callers can share it (e.g. with mount_mcp).
|
|
320
|
-
router.sessions = sessions
|
|
321
|
-
|
|
322
|
-
return router
|
|
323
|
-
|
|
324
427
|
|
|
325
428
|
def _wire_jobs_lifespan(app: FastAPI, orchestrators: list) -> None:
|
|
326
429
|
"""Start/stop the per-agent job orchestrators inside the app's lifespan.
|
|
@@ -360,6 +463,7 @@ def create_app(
|
|
|
360
463
|
description: Optional[str] = None,
|
|
361
464
|
model: Optional[str] = None,
|
|
362
465
|
mcp: bool = False,
|
|
466
|
+
sessions: bool = True,
|
|
363
467
|
jobs: bool = False,
|
|
364
468
|
) -> FastAPI:
|
|
365
469
|
"""Build a standalone FastAPI app serving one or several agents.
|
|
@@ -388,6 +492,10 @@ def create_app(
|
|
|
388
492
|
model (Optional[str]): Default model for all agents' sessions. Overrides
|
|
389
493
|
config.
|
|
390
494
|
mcp (bool): If True, mount an MCP endpoint per agent at ``<prefix>/mcp``.
|
|
495
|
+
sessions (bool): If True (default), mount the session-based routes
|
|
496
|
+
(sessions CRUD plus synchronous/streaming chat and state) for each
|
|
497
|
+
agent. If False, those routes are omitted — pair with ``jobs`` to
|
|
498
|
+
serve agents purely through the async job API.
|
|
391
499
|
jobs (bool): If True (or ``[jobs].enabled`` in config), mount a durable
|
|
392
500
|
async job system per agent at ``<prefix>/jobs`` — runs submitted there
|
|
393
501
|
execute in the background and survive client timeouts/reconnects.
|
|
@@ -420,7 +528,24 @@ def create_app(
|
|
|
420
528
|
index = []
|
|
421
529
|
orchestrators = []
|
|
422
530
|
for prefix, agent_class in mapping.items():
|
|
423
|
-
|
|
531
|
+
# The root agent inherits the app's display name; agents under a prefix
|
|
532
|
+
# take a name derived from that prefix, so each router's route names stay
|
|
533
|
+
# unique across the app.
|
|
534
|
+
agent_name = name if prefix == "" else prefix.strip("/").replace("/", "-")
|
|
535
|
+
jobs_cfg = None
|
|
536
|
+
if jobs_enabled:
|
|
537
|
+
jobs_cfg = config.jobs.model_copy(
|
|
538
|
+
update={"store": _store_path_for(config.jobs.store, prefix, multi)}
|
|
539
|
+
)
|
|
540
|
+
router = create_router(
|
|
541
|
+
agent_class,
|
|
542
|
+
model=model,
|
|
543
|
+
name=agent_name,
|
|
544
|
+
version=version,
|
|
545
|
+
sessions=sessions,
|
|
546
|
+
jobs=jobs_enabled,
|
|
547
|
+
jobs_config=jobs_cfg,
|
|
548
|
+
)
|
|
424
549
|
app.include_router(router, prefix=prefix)
|
|
425
550
|
index.append({"agent_class": agent_class.__name__, "prefix": prefix or "/"})
|
|
426
551
|
if mcp:
|
|
@@ -432,23 +557,8 @@ def create_app(
|
|
|
432
557
|
sessions=router.sessions,
|
|
433
558
|
path=(prefix + "/mcp"),
|
|
434
559
|
)
|
|
435
|
-
if
|
|
436
|
-
|
|
437
|
-
from pyagentic.api.jobs._routes import build_jobs_router
|
|
438
|
-
|
|
439
|
-
store = build_store(_store_path_for(config.jobs.store, prefix, multi))
|
|
440
|
-
backend = build_backend(
|
|
441
|
-
agent_class,
|
|
442
|
-
router.sessions,
|
|
443
|
-
max_concurrency=config.jobs.max_concurrency,
|
|
444
|
-
default_model=model,
|
|
445
|
-
)
|
|
446
|
-
orchestrator = JobOrchestrator(store, backend, config.jobs, router.sessions)
|
|
447
|
-
app.include_router(
|
|
448
|
-
build_jobs_router(orchestrator, agent_class, router.sessions),
|
|
449
|
-
prefix=prefix,
|
|
450
|
-
)
|
|
451
|
-
orchestrators.append(orchestrator)
|
|
560
|
+
if router.orchestrator is not None:
|
|
561
|
+
orchestrators.append(router.orchestrator)
|
|
452
562
|
|
|
453
563
|
if orchestrators:
|
|
454
564
|
_wire_jobs_lifespan(app, orchestrators)
|
|
@@ -150,3 +150,24 @@ def test_jobs_enabled_multi_agent_prefixed():
|
|
|
150
150
|
paths = {getattr(r, "path", None) for r in app.routes}
|
|
151
151
|
assert "/app-test/jobs" in paths
|
|
152
152
|
assert "/writer/jobs" in paths
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---- sessions opt-out ----
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_sessions_disabled():
|
|
159
|
+
"""sessions=False omits the session routes from the app."""
|
|
160
|
+
app = create_app(_AppTestAgent, model="_mock::test-model", sessions=False)
|
|
161
|
+
paths = {getattr(r, "path", None) for r in app.routes}
|
|
162
|
+
assert "/sessions" not in paths
|
|
163
|
+
assert "/schema" in paths # info routes remain
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_sessions_off_jobs_on():
|
|
167
|
+
"""An async-only app: no /sessions, but /jobs is served."""
|
|
168
|
+
app = create_app(
|
|
169
|
+
_AppTestAgent, model="_mock::test-model", sessions=False, jobs=True
|
|
170
|
+
)
|
|
171
|
+
paths = {getattr(r, "path", None) for r in app.routes}
|
|
172
|
+
assert "/sessions" not in paths
|
|
173
|
+
assert "/jobs" in paths
|
|
@@ -79,3 +79,66 @@ def test_two_routers_isolated_sessions():
|
|
|
79
79
|
sid = client.post("/a/sessions").json()["session_id"]
|
|
80
80
|
assert sid in client.get("/a/sessions").json()["sessions"]
|
|
81
81
|
assert sid not in client.get("/b/sessions").json()["sessions"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_routes_are_named():
|
|
85
|
+
"""Each route gets a name derived from the agent name, for url reversal."""
|
|
86
|
+
router = create_router(_RouterTestAgent, name="my-agent", model="_mock::test-model")
|
|
87
|
+
names = {route.name for route in router.routes}
|
|
88
|
+
assert {"my-agent_info", "my-agent_chat", "my-agent_get_state"} <= names
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_tags_applied():
|
|
92
|
+
"""The tags arg is attached to every route on the router."""
|
|
93
|
+
router = create_router(
|
|
94
|
+
_RouterTestAgent, model="_mock::test-model", tags=["bots"]
|
|
95
|
+
)
|
|
96
|
+
assert all("bots" in route.tags for route in router.routes)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_named_routes_unique_across_mounts():
|
|
100
|
+
"""Distinct names keep route names unique when mounted on one app."""
|
|
101
|
+
app = FastAPI()
|
|
102
|
+
app.include_router(
|
|
103
|
+
create_router(_RouterTestAgent, name="a", model="_mock::test-model"), prefix="/a"
|
|
104
|
+
)
|
|
105
|
+
app.include_router(
|
|
106
|
+
create_router(_RouterTestAgent, name="b", model="_mock::test-model"), prefix="/b"
|
|
107
|
+
)
|
|
108
|
+
# url_path_for resolves each agent's chat route by its unique name.
|
|
109
|
+
assert app.url_path_for("a_chat", session_id="x") == "/a/sessions/x/chat"
|
|
110
|
+
assert app.url_path_for("b_chat", session_id="x") == "/b/sessions/x/chat"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_sessions_disabled_omits_session_routes():
|
|
114
|
+
"""sessions=False drops the /sessions routes but keeps info/health/schema."""
|
|
115
|
+
app = FastAPI()
|
|
116
|
+
app.include_router(
|
|
117
|
+
create_router(_RouterTestAgent, model="_mock::test-model", sessions=False),
|
|
118
|
+
prefix=PREFIX,
|
|
119
|
+
)
|
|
120
|
+
client = TestClient(app)
|
|
121
|
+
assert client.get(f"{PREFIX}/health").json() == {"status": "ok"}
|
|
122
|
+
assert client.post(f"{PREFIX}/sessions").status_code == 404
|
|
123
|
+
assert client.get(f"{PREFIX}/sessions").status_code == 404
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_jobs_router_and_orchestrator_exposed():
|
|
127
|
+
"""jobs=True mounts /jobs and exposes the orchestrator on the router."""
|
|
128
|
+
router = create_router(_RouterTestAgent, model="_mock::test-model", jobs=True)
|
|
129
|
+
assert router.orchestrator is not None
|
|
130
|
+
paths = {getattr(r, "path", None) for r in router.routes}
|
|
131
|
+
assert "/jobs" in paths
|
|
132
|
+
# Without jobs, no orchestrator.
|
|
133
|
+
plain = create_router(_RouterTestAgent, model="_mock::test-model")
|
|
134
|
+
assert plain.orchestrator is None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_sessions_off_jobs_on():
|
|
138
|
+
"""sessions=False with jobs=True serves an async-only agent."""
|
|
139
|
+
router = create_router(
|
|
140
|
+
_RouterTestAgent, model="_mock::test-model", sessions=False, jobs=True
|
|
141
|
+
)
|
|
142
|
+
paths = {getattr(r, "path", None) for r in router.routes}
|
|
143
|
+
assert "/jobs" in paths
|
|
144
|
+
assert "/sessions" not in paths
|
|
File without changes
|
{pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/.github/ISSUE_TEMPLATE/feature_request.md
RENAMED
|
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
|
{pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/_base/_agent/_agent_linking.py
RENAMED
|
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
|
{pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/backends/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic/api/jobs/backends/_in_process.py
RENAMED
|
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
|
{pyagentic_core-2.8.1.dev1 → pyagentic_core-2.9.0a1}/pyagentic_core.egg-info/dependency_links.txt
RENAMED
|
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
|