hai-agent-api 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_api/__init__.py +28 -0
- agent_api/config.py +31 -0
- agent_api/deps/auth.py +100 -0
- agent_api/deps/pagination.py +31 -0
- agent_api/exceptions.py +5 -0
- agent_api/models/__init__.py +32 -0
- agent_api/models/memory.py +45 -0
- agent_api/models/quota.py +12 -0
- agent_api/models/requests.py +33 -0
- agent_api/models/session.py +68 -0
- agent_api/routers/__init__.py +29 -0
- agent_api/routers/agents.py +75 -0
- agent_api/routers/environments.py +77 -0
- agent_api/routers/memories.py +76 -0
- agent_api/routers/sessions.py +243 -0
- agent_api/routers/skills.py +69 -0
- agent_api/services/__init__.py +22 -0
- agent_api/services/protocols.py +228 -0
- agent_api/user.py +24 -0
- agent_interface/__init__.py +63 -0
- agent_interface/defaults.py +73 -0
- agent_interface/definition.py +320 -0
- agent_interface/errors.py +20 -0
- agent_interface/interaction/__init__.py +1 -0
- agent_interface/interaction/events.py +84 -0
- agent_interface/interaction/resources.py +57 -0
- agent_interface/interaction/resources_storage.py +141 -0
- agent_interface/interaction/screenshots_storage.py +44 -0
- agent_interface/interaction/user.py +67 -0
- agent_interface/py.typed +0 -0
- agent_interface/specs/__init__.py +30 -0
- agent_interface/specs/_naming.py +17 -0
- agent_interface/specs/agent.py +99 -0
- agent_interface/specs/environment.py +121 -0
- agent_interface/specs/overrides.py +230 -0
- agent_interface/specs/session.py +92 -0
- agent_interface/specs/skill.py +69 -0
- agent_interface/status.py +35 -0
- agent_interface/trajectory.py +60 -0
- agp_types/__init__.py +84 -0
- agp_types/agents.py +61 -0
- agp_types/changes.py +37 -0
- agp_types/environment.py +63 -0
- agp_types/events.py +59 -0
- agp_types/interaction.py +9 -0
- agp_types/pagination.py +27 -0
- agp_types/py.typed +0 -0
- agp_types/status.py +5 -0
- agp_types/task_type.py +10 -0
- agp_types/trajectory.py +162 -0
- agp_types/user.py +5 -0
- hai_agent_api-0.1.0.dist-info/METADATA +12 -0
- hai_agent_api-0.1.0.dist-info/RECORD +54 -0
- hai_agent_api-0.1.0.dist-info/WHEEL +4 -0
agent_api/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Shared FastAPI surface for the H Agent Platform agents API."""
|
|
2
|
+
|
|
3
|
+
from agent_api.config import ApiConfig, AuthConfig
|
|
4
|
+
from agent_api.routers import create_router
|
|
5
|
+
from agent_api.services import Services
|
|
6
|
+
from agent_api.services.protocols import (
|
|
7
|
+
AgentServiceProtocol,
|
|
8
|
+
EnvironmentServiceProtocol,
|
|
9
|
+
MemoryServiceProtocol,
|
|
10
|
+
SessionServiceProtocol,
|
|
11
|
+
SkillServiceProtocol,
|
|
12
|
+
)
|
|
13
|
+
from agent_api.user import ApiUser, OrgId, UserId
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AgentServiceProtocol",
|
|
17
|
+
"ApiConfig",
|
|
18
|
+
"AuthConfig",
|
|
19
|
+
"ApiUser",
|
|
20
|
+
"EnvironmentServiceProtocol",
|
|
21
|
+
"MemoryServiceProtocol",
|
|
22
|
+
"OrgId",
|
|
23
|
+
"Services",
|
|
24
|
+
"SessionServiceProtocol",
|
|
25
|
+
"SkillServiceProtocol",
|
|
26
|
+
"UserId",
|
|
27
|
+
"create_router",
|
|
28
|
+
]
|
agent_api/config.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
DEFAULT_LOCAL_USER_ID = UUID("00000000-0000-0000-0000-000000000001")
|
|
7
|
+
DEFAULT_LOCAL_ORG_ID = UUID("00000000-0000-0000-0000-000000000002")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthConfig(BaseModel):
|
|
11
|
+
"""Authentication behavior for ``create_router``.
|
|
12
|
+
|
|
13
|
+
``platform`` requires ``X-User-Sub`` and ``X-User-Org`` (as injected by the Agent Platform gateway).
|
|
14
|
+
``local`` fills missing identity headers with ``default_user_id`` / ``default_org_id`` for protected routes.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
mode: Literal["platform", "local"] = "platform"
|
|
18
|
+
default_user_id: UUID = Field(default=DEFAULT_LOCAL_USER_ID)
|
|
19
|
+
default_org_id: UUID = Field(default=DEFAULT_LOCAL_ORG_ID)
|
|
20
|
+
default_email: str = "local-dev@example.com"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ApiConfig(BaseModel):
|
|
24
|
+
"""Runtime configuration for agents API routers."""
|
|
25
|
+
|
|
26
|
+
long_poll_max_wait_for_seconds: int = Field(default=60, ge=0)
|
|
27
|
+
session_share_url_template: str = Field(
|
|
28
|
+
default="/share/api/v1/trajectories/{session_id}",
|
|
29
|
+
description="Path template for public session share links; ``{session_id}`` is substituted.",
|
|
30
|
+
)
|
|
31
|
+
auth: AuthConfig = Field(default_factory=AuthConfig)
|
agent_api/deps/auth.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Awaitable, Callable
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from agent_api.config import AuthConfig
|
|
9
|
+
from agent_api.user import ApiUser
|
|
10
|
+
from fastapi import Depends, Header, HTTPException, Security
|
|
11
|
+
from fastapi.security import HTTPBearer
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
AuthenticateDep = Callable[..., Awaitable[ApiUser]]
|
|
16
|
+
OptionalAuthenticateDep = Callable[..., Awaitable[ApiUser | None]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class AuthDeps:
|
|
21
|
+
"""FastAPI dependencies wired into agents API routers."""
|
|
22
|
+
|
|
23
|
+
authenticate: AuthenticateDep
|
|
24
|
+
optional_authenticate: OptionalAuthenticateDep
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_get_user(
|
|
28
|
+
*,
|
|
29
|
+
default_user_id: UUID | None = None,
|
|
30
|
+
default_org_id: UUID | None = None,
|
|
31
|
+
default_email: str = "",
|
|
32
|
+
) -> Callable[..., ApiUser | None]:
|
|
33
|
+
def _get_user(
|
|
34
|
+
user_id: Annotated[str | None, Header(alias="X-User-Sub", include_in_schema=False)] = None,
|
|
35
|
+
org_id: Annotated[str | None, Header(alias="X-User-Org", include_in_schema=False)] = None,
|
|
36
|
+
role: Annotated[str | None, Header(alias="X-User-Role", include_in_schema=False)] = None,
|
|
37
|
+
org_role: Annotated[str | None, Header(alias="X-User-Org-Role", include_in_schema=False)] = None,
|
|
38
|
+
email: Annotated[str | None, Header(alias="X-User-Email", include_in_schema=False)] = None,
|
|
39
|
+
_: Annotated[HTTPBearer, Depends(HTTPBearer(auto_error=False))] = None,
|
|
40
|
+
) -> ApiUser | None:
|
|
41
|
+
if default_user_id is not None and default_org_id is not None:
|
|
42
|
+
user_id = user_id or str(default_user_id)
|
|
43
|
+
org_id = org_id or str(default_org_id)
|
|
44
|
+
if email is None:
|
|
45
|
+
email = default_email
|
|
46
|
+
elif not user_id or not org_id:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
if email is None:
|
|
50
|
+
email = ""
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
return ApiUser(id=UUID(user_id), org_id=UUID(org_id), role=role, org_role=org_role, email=email)
|
|
54
|
+
except ValueError:
|
|
55
|
+
logger.warning("Invalid UUID in auth headers: user_id=%s, org_id=%s", user_id, org_id)
|
|
56
|
+
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail="Invalid user or org ID") from None
|
|
57
|
+
|
|
58
|
+
return _get_user
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _make_authenticate(get_user: Callable[..., ApiUser | None]) -> AuthenticateDep:
|
|
62
|
+
async def authenticate(user: Annotated[ApiUser | None, Security(get_user)]) -> ApiUser:
|
|
63
|
+
"""Extract and validate the authenticated user."""
|
|
64
|
+
if user is None:
|
|
65
|
+
raise HTTPException(status_code=401, detail="Authentication required")
|
|
66
|
+
return user
|
|
67
|
+
|
|
68
|
+
return authenticate
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _make_optional_authenticate(get_user: Callable[..., ApiUser | None]) -> OptionalAuthenticateDep:
|
|
72
|
+
async def optional_authenticate(
|
|
73
|
+
user: Annotated[ApiUser | None, Security(get_user)],
|
|
74
|
+
) -> ApiUser | None:
|
|
75
|
+
"""Extract and validate the user when auth headers are present."""
|
|
76
|
+
return user
|
|
77
|
+
|
|
78
|
+
return optional_authenticate
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_platform_get_user = _make_get_user()
|
|
82
|
+
authenticate = _make_authenticate(_platform_get_user)
|
|
83
|
+
optional_authenticate = _make_optional_authenticate(_platform_get_user)
|
|
84
|
+
PLATFORM_AUTH = AuthDeps(authenticate=authenticate, optional_authenticate=optional_authenticate)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_auth_deps(config: AuthConfig) -> AuthDeps:
|
|
88
|
+
"""Build router auth dependencies for the given configuration."""
|
|
89
|
+
if config.mode == "platform":
|
|
90
|
+
return PLATFORM_AUTH
|
|
91
|
+
|
|
92
|
+
get_user_with_defaults = _make_get_user(
|
|
93
|
+
default_user_id=config.default_user_id,
|
|
94
|
+
default_org_id=config.default_org_id,
|
|
95
|
+
default_email=config.default_email,
|
|
96
|
+
)
|
|
97
|
+
return AuthDeps(
|
|
98
|
+
authenticate=_make_authenticate(get_user_with_defaults),
|
|
99
|
+
optional_authenticate=optional_authenticate,
|
|
100
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Any, Callable, Coroutine, Literal
|
|
2
|
+
|
|
3
|
+
from agp_types import PageRequest
|
|
4
|
+
from fastapi import Query
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def page_requester(
|
|
8
|
+
sort_fields: list[str] | None = None, default_sort: list[str] | None = None
|
|
9
|
+
) -> Callable[[], Coroutine[Any, Any, PageRequest]]:
|
|
10
|
+
"""Build a FastAPI dependency that parses pagination query parameters."""
|
|
11
|
+
page_query = Query(1, ge=1, description="Page number (1-based)")
|
|
12
|
+
size_query = Query(10, ge=1, le=1000, description="Number of items per page")
|
|
13
|
+
|
|
14
|
+
if sort_fields:
|
|
15
|
+
SortField = Literal[tuple(sort_fields)] # type: ignore[valid-type]
|
|
16
|
+
|
|
17
|
+
async def sorted_fn(
|
|
18
|
+
page: int = page_query,
|
|
19
|
+
size: int = size_query,
|
|
20
|
+
sort: list[SortField] | None = Query(default_sort, description="Sort by field"), # type: ignore[valid-type]
|
|
21
|
+
) -> PageRequest:
|
|
22
|
+
if sort is None and default_sort:
|
|
23
|
+
sort = default_sort
|
|
24
|
+
return PageRequest(page=page, size=size, sort=sort)
|
|
25
|
+
|
|
26
|
+
return sorted_fn
|
|
27
|
+
|
|
28
|
+
async def fn(page: int = page_query, size: int = size_query) -> PageRequest:
|
|
29
|
+
return PageRequest(page=page, size=size, sort=default_sort)
|
|
30
|
+
|
|
31
|
+
return fn
|
agent_api/exceptions.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Module to describe the models."""
|
|
2
|
+
|
|
3
|
+
from agent_api.models.memory import (
|
|
4
|
+
MAX_KEY_LENGTH,
|
|
5
|
+
MAX_NAMESPACE_LENGTH,
|
|
6
|
+
MAX_VALUE_BYTES,
|
|
7
|
+
CreateMemory,
|
|
8
|
+
MemoryRecord,
|
|
9
|
+
MemoryValue,
|
|
10
|
+
UpdateMemory,
|
|
11
|
+
)
|
|
12
|
+
from agent_api.models.quota import QuotaStatus
|
|
13
|
+
from agent_api.models.requests import SessionEventsPageRequest, SessionsPageRequest
|
|
14
|
+
from agent_api.models.session import SendMessage, Session, SessionSummary, ShareLink, UserMessageBatch
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CreateMemory",
|
|
18
|
+
"MAX_KEY_LENGTH",
|
|
19
|
+
"MAX_NAMESPACE_LENGTH",
|
|
20
|
+
"MAX_VALUE_BYTES",
|
|
21
|
+
"MemoryRecord",
|
|
22
|
+
"MemoryValue",
|
|
23
|
+
"QuotaStatus",
|
|
24
|
+
"SendMessage",
|
|
25
|
+
"Session",
|
|
26
|
+
"SessionEventsPageRequest",
|
|
27
|
+
"SessionSummary",
|
|
28
|
+
"SessionsPageRequest",
|
|
29
|
+
"ShareLink",
|
|
30
|
+
"UpdateMemory",
|
|
31
|
+
"UserMessageBatch",
|
|
32
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Public API models for the Memory resource (per-org KV, scoped by namespace)."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from pydantic import AfterValidator, BaseModel, Field
|
|
8
|
+
|
|
9
|
+
MAX_NAMESPACE_LENGTH = 255
|
|
10
|
+
MAX_KEY_LENGTH = 1024
|
|
11
|
+
MAX_VALUE_BYTES = 65536
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _cap_value_bytes(v: str) -> str:
|
|
15
|
+
if len(v.encode("utf-8")) > MAX_VALUE_BYTES:
|
|
16
|
+
raise ValueError(f"value exceeds {MAX_VALUE_BYTES}-byte cap")
|
|
17
|
+
return v
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MemoryValue = Annotated[str, AfterValidator(_cap_value_bytes)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CreateMemory(BaseModel):
|
|
24
|
+
"""Upsert a memory by ``(org_id, namespace, key)``."""
|
|
25
|
+
|
|
26
|
+
namespace: str = Field(..., min_length=1, max_length=MAX_NAMESPACE_LENGTH)
|
|
27
|
+
key: str = Field(..., min_length=1, max_length=MAX_KEY_LENGTH)
|
|
28
|
+
value: MemoryValue
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UpdateMemory(BaseModel):
|
|
32
|
+
"""Update a memory's value in place."""
|
|
33
|
+
|
|
34
|
+
value: MemoryValue
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MemoryRecord(BaseModel):
|
|
38
|
+
"""Persistent KV entry scoped to ``(org_id, namespace, key)``."""
|
|
39
|
+
|
|
40
|
+
id: UUID
|
|
41
|
+
namespace: str
|
|
42
|
+
key: str
|
|
43
|
+
value: str
|
|
44
|
+
created_at: datetime
|
|
45
|
+
updated_at: datetime
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from agp_types import PrivateScope, TrajectoryStatus
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SessionsPageRequest(BaseModel):
|
|
9
|
+
"""Page request for ``GET /v2/sessions``."""
|
|
10
|
+
|
|
11
|
+
page: int = Field(default=1, ge=1)
|
|
12
|
+
size: int = Field(default=10, ge=1, le=100)
|
|
13
|
+
sort: list[Literal["created_at", "-created_at"]] | None = None
|
|
14
|
+
|
|
15
|
+
owner: PrivateScope = "me-in-organization"
|
|
16
|
+
status: list[TrajectoryStatus] | None = None
|
|
17
|
+
agent: list[str] | None = None
|
|
18
|
+
group_id: str | None = None
|
|
19
|
+
parent_session_id: str | None = None
|
|
20
|
+
search: str | None = None
|
|
21
|
+
created_before: datetime.datetime | None = None
|
|
22
|
+
created_after: datetime.datetime | None = None
|
|
23
|
+
finished_before: datetime.datetime | None = None
|
|
24
|
+
finished_after: datetime.datetime | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SessionEventsPageRequest(BaseModel):
|
|
28
|
+
"""Page request for ``GET /v2/sessions/{id}/events``."""
|
|
29
|
+
|
|
30
|
+
page: int = Field(default=1, ge=1)
|
|
31
|
+
size: int = Field(default=50, ge=1, le=200)
|
|
32
|
+
sort: list[Literal["timestamp", "-timestamp"]] | None = None
|
|
33
|
+
type: str | None = None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Public ``Session`` models."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Annotated, Any, Literal
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from agent_interface import SessionStatus, TrajectoryStatus
|
|
8
|
+
from agent_interface.definition import UserMessageEvent
|
|
9
|
+
from agent_interface.specs.session import SessionRequest
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Session(BaseModel):
|
|
14
|
+
"""Full session envelope: original request + live status."""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(extra="forbid")
|
|
17
|
+
|
|
18
|
+
id: UUID
|
|
19
|
+
request: SessionRequest
|
|
20
|
+
status: SessionStatus
|
|
21
|
+
latest_answer: Any = Field(
|
|
22
|
+
default=None,
|
|
23
|
+
description=(
|
|
24
|
+
"The agent's most recent final answer: free-form text, or structured data when the "
|
|
25
|
+
"agent runs with a custom answer format. Null until the agent first answers. Mirrors the "
|
|
26
|
+
"answer streamed from the changes endpoint, surfaced here for non-interactive runs."
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
created_at: datetime
|
|
30
|
+
started_at: datetime | None = None
|
|
31
|
+
finished_at: datetime | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SessionSummary(BaseModel):
|
|
35
|
+
"""Flat projection for session listings, including child rosters via ``?parent_session_id=``."""
|
|
36
|
+
|
|
37
|
+
model_config = ConfigDict(extra="forbid")
|
|
38
|
+
|
|
39
|
+
id: UUID
|
|
40
|
+
agent: str | None = None
|
|
41
|
+
status: TrajectoryStatus
|
|
42
|
+
first_message: UserMessageEvent | None = None
|
|
43
|
+
created_at: datetime
|
|
44
|
+
started_at: datetime | None = None
|
|
45
|
+
finished_at: datetime | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UserMessageBatch(BaseModel):
|
|
49
|
+
"""Batch of user messages."""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(extra="forbid")
|
|
52
|
+
|
|
53
|
+
type: Literal["batch"] = "batch"
|
|
54
|
+
messages: Annotated[list[UserMessageEvent], Field(min_length=1)]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
SendMessage = Annotated[
|
|
58
|
+
UserMessageEvent | UserMessageBatch,
|
|
59
|
+
Field(discriminator="type"),
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ShareLink(BaseModel):
|
|
64
|
+
"""Public share URL for a session."""
|
|
65
|
+
|
|
66
|
+
model_config = ConfigDict(extra="forbid")
|
|
67
|
+
|
|
68
|
+
share_url: str
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Module to describe the routers."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
|
|
5
|
+
from agent_api.config import ApiConfig, AuthConfig
|
|
6
|
+
from agent_api.deps.auth import build_auth_deps
|
|
7
|
+
from agent_api.routers.agents import create_agents_router
|
|
8
|
+
from agent_api.routers.environments import create_environments_router
|
|
9
|
+
from agent_api.routers.memories import create_memories_router
|
|
10
|
+
from agent_api.routers.sessions import create_sessions_router
|
|
11
|
+
from agent_api.routers.skills import create_skills_router
|
|
12
|
+
from agent_api.services import Services
|
|
13
|
+
|
|
14
|
+
__all__ = ["ApiConfig", "AuthConfig", "Services", "create_router"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_router(services: Services, *, config: ApiConfig | None = None) -> APIRouter:
|
|
18
|
+
"""Build the agents API router tree with versioned path prefixes."""
|
|
19
|
+
resolved_config = config or ApiConfig()
|
|
20
|
+
auth = build_auth_deps(resolved_config.auth)
|
|
21
|
+
v2 = APIRouter()
|
|
22
|
+
v2.include_router(create_sessions_router(services.sessions, resolved_config, auth), prefix="/sessions")
|
|
23
|
+
v2.include_router(create_memories_router(services.memories, auth), prefix="/memories", include_in_schema=False)
|
|
24
|
+
v2.include_router(create_skills_router(services.skills, auth), prefix="/skills")
|
|
25
|
+
v2.include_router(create_environments_router(services.environments, auth), prefix="/environments")
|
|
26
|
+
v2.include_router(create_agents_router(services.agents, auth), prefix="/agents")
|
|
27
|
+
router = APIRouter()
|
|
28
|
+
router.include_router(v2, prefix="/v2")
|
|
29
|
+
return router
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Agent catalog endpoints."""
|
|
2
|
+
|
|
3
|
+
from agent_interface.specs.agent import Agent
|
|
4
|
+
from agp_types import Page, PageRequest
|
|
5
|
+
from fastapi import APIRouter, Depends, Query
|
|
6
|
+
from typing_extensions import Annotated
|
|
7
|
+
|
|
8
|
+
from agent_api.deps.auth import PLATFORM_AUTH, AuthDeps
|
|
9
|
+
from agent_api.deps.pagination import page_requester
|
|
10
|
+
from agent_api.services.protocols import AgentServiceProtocol
|
|
11
|
+
from agent_api.user import ApiUser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_agents_router(service: AgentServiceProtocol, auth: AuthDeps = PLATFORM_AUTH) -> APIRouter:
|
|
15
|
+
"""Create the agents router bound to ``service``."""
|
|
16
|
+
api = APIRouter(tags=["Agents"])
|
|
17
|
+
|
|
18
|
+
@api.post("", status_code=201)
|
|
19
|
+
async def create_agent(
|
|
20
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
21
|
+
create: Agent,
|
|
22
|
+
) -> Agent:
|
|
23
|
+
"""Create an agent.."""
|
|
24
|
+
return await service.create_agent(user, create)
|
|
25
|
+
|
|
26
|
+
@api.get("")
|
|
27
|
+
async def list_agents(
|
|
28
|
+
page: Annotated[
|
|
29
|
+
PageRequest,
|
|
30
|
+
Depends(
|
|
31
|
+
page_requester(
|
|
32
|
+
sort_fields=["created_at", "-created_at", "agent_name", "-agent_name"],
|
|
33
|
+
default_sort=["-created_at"],
|
|
34
|
+
)
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
38
|
+
agent_name: Annotated[str | None, Query(description="Case-insensitive substring match on agent name.")] = None,
|
|
39
|
+
search: Annotated[str | None, Query(description="Case-insensitive match on agent name or description.")] = None,
|
|
40
|
+
) -> Page[Agent]:
|
|
41
|
+
"""List reserved + caller's org agents."""
|
|
42
|
+
return await service.get_page(user, page, agent_name=agent_name, search=search)
|
|
43
|
+
|
|
44
|
+
@api.get("/{agent_name:path}")
|
|
45
|
+
async def get_agent(
|
|
46
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
47
|
+
agent_name: str,
|
|
48
|
+
resolve: Annotated[
|
|
49
|
+
bool, Query(description="Materialise string environment/skill/subagent leaves into full specs.")
|
|
50
|
+
] = False,
|
|
51
|
+
) -> Agent:
|
|
52
|
+
"""Fetch by identifier; 404 if not visible. ``resolve=true`` materialises spec leaves."""
|
|
53
|
+
agent = await service.get_agent(user, agent_name)
|
|
54
|
+
if resolve:
|
|
55
|
+
agent = await service.resolve_agent_spec(user, agent)
|
|
56
|
+
return agent
|
|
57
|
+
|
|
58
|
+
@api.put("/{agent_name:path}")
|
|
59
|
+
async def update_agent(
|
|
60
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
61
|
+
agent_name: str,
|
|
62
|
+
update: Agent,
|
|
63
|
+
) -> Agent:
|
|
64
|
+
"""Replace ``spec``. ``spec.name`` must match the URL identifier; renames are not supported."""
|
|
65
|
+
return await service.update_agent(user, agent_name, update)
|
|
66
|
+
|
|
67
|
+
@api.delete("/{agent_name:path}", status_code=204)
|
|
68
|
+
async def delete_agent(
|
|
69
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
70
|
+
agent_name: str,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Delete by identifier. Reserved rows: H employee only."""
|
|
73
|
+
await service.delete_agent(user, agent_name)
|
|
74
|
+
|
|
75
|
+
return api
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Environment catalog endpoints."""
|
|
2
|
+
|
|
3
|
+
from agent_interface.specs.environment import Environment, EnvironmentKind
|
|
4
|
+
from agp_types import Page, PageRequest
|
|
5
|
+
from fastapi import APIRouter, Depends, Query
|
|
6
|
+
from typing_extensions import Annotated
|
|
7
|
+
|
|
8
|
+
from agent_api.deps.auth import PLATFORM_AUTH, AuthDeps
|
|
9
|
+
from agent_api.deps.pagination import page_requester
|
|
10
|
+
from agent_api.services.protocols import EnvironmentServiceProtocol
|
|
11
|
+
from agent_api.user import ApiUser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EnvironmentPage(Page[Environment]):
|
|
15
|
+
"""Named page subclass so OpenAPI emits a clean ``EnvironmentPage`` schema name."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_environments_router(service: EnvironmentServiceProtocol, auth: AuthDeps = PLATFORM_AUTH) -> APIRouter:
|
|
19
|
+
"""Create the environments router bound to ``service``."""
|
|
20
|
+
api = APIRouter(tags=["Environments"])
|
|
21
|
+
|
|
22
|
+
@api.post("", status_code=201)
|
|
23
|
+
async def create_environment(
|
|
24
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
25
|
+
create: Environment,
|
|
26
|
+
) -> Environment:
|
|
27
|
+
"""Create an environment."""
|
|
28
|
+
return await service.create_environment(user, create)
|
|
29
|
+
|
|
30
|
+
@api.get("")
|
|
31
|
+
async def list_environments(
|
|
32
|
+
page: Annotated[
|
|
33
|
+
PageRequest,
|
|
34
|
+
Depends(
|
|
35
|
+
page_requester(
|
|
36
|
+
sort_fields=["created_at", "-created_at", "id", "-id"],
|
|
37
|
+
default_sort=["-created_at"],
|
|
38
|
+
)
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
42
|
+
id: Annotated[str | None, Query(description="Case-insensitive substring match on environment id.")] = None,
|
|
43
|
+
kind: Annotated[EnvironmentKind | None, Query(description="Filter by environment kind.")] = None,
|
|
44
|
+
search: Annotated[
|
|
45
|
+
str | None, Query(description="Case-insensitive match on environment id or description.")
|
|
46
|
+
] = None,
|
|
47
|
+
) -> EnvironmentPage:
|
|
48
|
+
"""List reserved + caller's org environments."""
|
|
49
|
+
result = await service.get_page(user, page, env_id=id, kind=kind, search=search)
|
|
50
|
+
return EnvironmentPage(items=result.items, total=result.total, page=result.page)
|
|
51
|
+
|
|
52
|
+
@api.get("/{id:path}")
|
|
53
|
+
async def get_environment(
|
|
54
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
55
|
+
id: str,
|
|
56
|
+
) -> Environment:
|
|
57
|
+
"""Fetch by identifier; 404 if not visible. ``:path`` so slash-containing ids round-trip."""
|
|
58
|
+
return await service.get_environment(user, id)
|
|
59
|
+
|
|
60
|
+
@api.put("/{id:path}")
|
|
61
|
+
async def update_environment(
|
|
62
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
63
|
+
id: str,
|
|
64
|
+
update: Environment,
|
|
65
|
+
) -> Environment:
|
|
66
|
+
"""Replace the environment spec."""
|
|
67
|
+
return await service.update_environment(user, id, update)
|
|
68
|
+
|
|
69
|
+
@api.delete("/{id:path}", status_code=204)
|
|
70
|
+
async def delete_environment(
|
|
71
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
72
|
+
id: str,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Delete by identifier. Reserved rows: H employee only."""
|
|
75
|
+
await service.delete_environment(user, id)
|
|
76
|
+
|
|
77
|
+
return api
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Memory API endpoints."""
|
|
2
|
+
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from agp_types import Page, PageRequest
|
|
6
|
+
from fastapi import APIRouter, Depends, Query, Response
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
|
+
|
|
9
|
+
from agent_api.deps.auth import PLATFORM_AUTH, AuthDeps
|
|
10
|
+
from agent_api.deps.pagination import page_requester
|
|
11
|
+
from agent_api.models.memory import CreateMemory, MemoryRecord, UpdateMemory
|
|
12
|
+
from agent_api.services.protocols import MemoryServiceProtocol
|
|
13
|
+
from agent_api.user import ApiUser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_memories_router(service: MemoryServiceProtocol, auth: AuthDeps = PLATFORM_AUTH) -> APIRouter:
|
|
17
|
+
"""Create the memories router bound to ``service``."""
|
|
18
|
+
api = APIRouter(tags=["Memories"])
|
|
19
|
+
|
|
20
|
+
@api.post(
|
|
21
|
+
"",
|
|
22
|
+
responses={
|
|
23
|
+
200: {"description": "Existing memory updated (upsert)."},
|
|
24
|
+
201: {"description": "New memory created."},
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
async def create_memory(
|
|
28
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
29
|
+
create: CreateMemory,
|
|
30
|
+
response: Response,
|
|
31
|
+
) -> MemoryRecord:
|
|
32
|
+
"""Upsert a memory by ``(org_id, namespace, key)``. 201 on create, 200 on update."""
|
|
33
|
+
memory, created = await service.upsert_memory(user, create)
|
|
34
|
+
response.status_code = 201 if created else 200
|
|
35
|
+
return memory
|
|
36
|
+
|
|
37
|
+
@api.get("")
|
|
38
|
+
async def list_memories(
|
|
39
|
+
page: Annotated[
|
|
40
|
+
PageRequest,
|
|
41
|
+
Depends(page_requester(sort_fields=["updated_at", "-updated_at"], default_sort=["-updated_at"])),
|
|
42
|
+
],
|
|
43
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
44
|
+
namespace: Annotated[str | None, Query(description="Exact namespace filter.")] = None,
|
|
45
|
+
key: Annotated[str | None, Query(description="Case-insensitive substring match on key.")] = None,
|
|
46
|
+
search: Annotated[str | None, Query(description="Case-insensitive match on key or value.")] = None,
|
|
47
|
+
) -> Page[MemoryRecord]:
|
|
48
|
+
"""List org memories; optional namespace, key, and text-search filters."""
|
|
49
|
+
return await service.get_page(user, page, namespace=namespace, key=key, search=search)
|
|
50
|
+
|
|
51
|
+
@api.get("/{id}")
|
|
52
|
+
async def get_memory(
|
|
53
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
54
|
+
id: UUID,
|
|
55
|
+
) -> MemoryRecord:
|
|
56
|
+
"""Get a memory by id."""
|
|
57
|
+
return await service.get_memory(user, id)
|
|
58
|
+
|
|
59
|
+
@api.put("/{id}")
|
|
60
|
+
async def update_memory(
|
|
61
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
62
|
+
id: UUID,
|
|
63
|
+
update: UpdateMemory,
|
|
64
|
+
) -> MemoryRecord:
|
|
65
|
+
"""Update a memory's value."""
|
|
66
|
+
return await service.update_memory(user, id, update)
|
|
67
|
+
|
|
68
|
+
@api.delete("/{id}", status_code=204)
|
|
69
|
+
async def delete_memory(
|
|
70
|
+
user: Annotated[ApiUser, Depends(auth.authenticate)],
|
|
71
|
+
id: UUID,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Delete a memory."""
|
|
74
|
+
await service.delete_memory(user, id)
|
|
75
|
+
|
|
76
|
+
return api
|