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.
Files changed (54) hide show
  1. agent_api/__init__.py +28 -0
  2. agent_api/config.py +31 -0
  3. agent_api/deps/auth.py +100 -0
  4. agent_api/deps/pagination.py +31 -0
  5. agent_api/exceptions.py +5 -0
  6. agent_api/models/__init__.py +32 -0
  7. agent_api/models/memory.py +45 -0
  8. agent_api/models/quota.py +12 -0
  9. agent_api/models/requests.py +33 -0
  10. agent_api/models/session.py +68 -0
  11. agent_api/routers/__init__.py +29 -0
  12. agent_api/routers/agents.py +75 -0
  13. agent_api/routers/environments.py +77 -0
  14. agent_api/routers/memories.py +76 -0
  15. agent_api/routers/sessions.py +243 -0
  16. agent_api/routers/skills.py +69 -0
  17. agent_api/services/__init__.py +22 -0
  18. agent_api/services/protocols.py +228 -0
  19. agent_api/user.py +24 -0
  20. agent_interface/__init__.py +63 -0
  21. agent_interface/defaults.py +73 -0
  22. agent_interface/definition.py +320 -0
  23. agent_interface/errors.py +20 -0
  24. agent_interface/interaction/__init__.py +1 -0
  25. agent_interface/interaction/events.py +84 -0
  26. agent_interface/interaction/resources.py +57 -0
  27. agent_interface/interaction/resources_storage.py +141 -0
  28. agent_interface/interaction/screenshots_storage.py +44 -0
  29. agent_interface/interaction/user.py +67 -0
  30. agent_interface/py.typed +0 -0
  31. agent_interface/specs/__init__.py +30 -0
  32. agent_interface/specs/_naming.py +17 -0
  33. agent_interface/specs/agent.py +99 -0
  34. agent_interface/specs/environment.py +121 -0
  35. agent_interface/specs/overrides.py +230 -0
  36. agent_interface/specs/session.py +92 -0
  37. agent_interface/specs/skill.py +69 -0
  38. agent_interface/status.py +35 -0
  39. agent_interface/trajectory.py +60 -0
  40. agp_types/__init__.py +84 -0
  41. agp_types/agents.py +61 -0
  42. agp_types/changes.py +37 -0
  43. agp_types/environment.py +63 -0
  44. agp_types/events.py +59 -0
  45. agp_types/interaction.py +9 -0
  46. agp_types/pagination.py +27 -0
  47. agp_types/py.typed +0 -0
  48. agp_types/status.py +5 -0
  49. agp_types/task_type.py +10 -0
  50. agp_types/trajectory.py +162 -0
  51. agp_types/user.py +5 -0
  52. hai_agent_api-0.1.0.dist-info/METADATA +12 -0
  53. hai_agent_api-0.1.0.dist-info/RECORD +54 -0
  54. 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
@@ -0,0 +1,5 @@
1
+ KEY_MAX_LEN = 255
2
+
3
+
4
+ class IdempotencyKeyConflict(Exception):
5
+ """Same Idempotency-Key, different request body."""
@@ -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,12 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class QuotaStatus(BaseModel):
7
+ """Quota status."""
8
+
9
+ scope: Literal["user", "org"]
10
+ limit: int
11
+ active: int
12
+ available: int
@@ -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