aegra-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.
- aegra_api/__init__.py +3 -0
- aegra_api/api/__init__.py +1 -0
- aegra_api/api/assistants.py +235 -0
- aegra_api/api/runs.py +1110 -0
- aegra_api/api/store.py +200 -0
- aegra_api/api/threads.py +761 -0
- aegra_api/config.py +204 -0
- aegra_api/constants.py +5 -0
- aegra_api/core/__init__.py +0 -0
- aegra_api/core/app_loader.py +91 -0
- aegra_api/core/auth_ctx.py +65 -0
- aegra_api/core/auth_deps.py +186 -0
- aegra_api/core/auth_handlers.py +248 -0
- aegra_api/core/auth_middleware.py +331 -0
- aegra_api/core/database.py +123 -0
- aegra_api/core/health.py +131 -0
- aegra_api/core/orm.py +165 -0
- aegra_api/core/route_merger.py +69 -0
- aegra_api/core/serializers/__init__.py +7 -0
- aegra_api/core/serializers/base.py +22 -0
- aegra_api/core/serializers/general.py +54 -0
- aegra_api/core/serializers/langgraph.py +102 -0
- aegra_api/core/sse.py +178 -0
- aegra_api/main.py +303 -0
- aegra_api/middleware/__init__.py +4 -0
- aegra_api/middleware/double_encoded_json.py +74 -0
- aegra_api/middleware/logger_middleware.py +95 -0
- aegra_api/models/__init__.py +76 -0
- aegra_api/models/assistants.py +81 -0
- aegra_api/models/auth.py +62 -0
- aegra_api/models/enums.py +29 -0
- aegra_api/models/errors.py +29 -0
- aegra_api/models/runs.py +124 -0
- aegra_api/models/store.py +67 -0
- aegra_api/models/threads.py +152 -0
- aegra_api/observability/__init__.py +1 -0
- aegra_api/observability/base.py +88 -0
- aegra_api/observability/otel.py +133 -0
- aegra_api/observability/setup.py +27 -0
- aegra_api/observability/targets/__init__.py +11 -0
- aegra_api/observability/targets/base.py +18 -0
- aegra_api/observability/targets/langfuse.py +33 -0
- aegra_api/observability/targets/otlp.py +38 -0
- aegra_api/observability/targets/phoenix.py +24 -0
- aegra_api/services/__init__.py +0 -0
- aegra_api/services/assistant_service.py +569 -0
- aegra_api/services/base_broker.py +59 -0
- aegra_api/services/broker.py +141 -0
- aegra_api/services/event_converter.py +157 -0
- aegra_api/services/event_store.py +196 -0
- aegra_api/services/graph_streaming.py +433 -0
- aegra_api/services/langgraph_service.py +456 -0
- aegra_api/services/streaming_service.py +362 -0
- aegra_api/services/thread_state_service.py +128 -0
- aegra_api/settings.py +124 -0
- aegra_api/utils/__init__.py +3 -0
- aegra_api/utils/assistants.py +23 -0
- aegra_api/utils/run_utils.py +60 -0
- aegra_api/utils/setup_logging.py +122 -0
- aegra_api/utils/sse_utils.py +26 -0
- aegra_api/utils/status_compat.py +57 -0
- aegra_api-0.1.0.dist-info/METADATA +244 -0
- aegra_api-0.1.0.dist-info/RECORD +64 -0
- aegra_api-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Agent Protocol Pydantic models"""
|
|
2
|
+
|
|
3
|
+
from aegra_api.models.assistants import (
|
|
4
|
+
AgentSchemas,
|
|
5
|
+
Assistant,
|
|
6
|
+
AssistantCreate,
|
|
7
|
+
AssistantList,
|
|
8
|
+
AssistantSearchRequest,
|
|
9
|
+
AssistantUpdate,
|
|
10
|
+
)
|
|
11
|
+
from aegra_api.models.auth import AuthContext, TokenPayload, User
|
|
12
|
+
from aegra_api.models.errors import AgentProtocolError, get_error_type
|
|
13
|
+
from aegra_api.models.runs import Run, RunCreate, RunStatus
|
|
14
|
+
from aegra_api.models.store import (
|
|
15
|
+
StoreDeleteRequest,
|
|
16
|
+
StoreGetResponse,
|
|
17
|
+
StoreItem,
|
|
18
|
+
StorePutRequest,
|
|
19
|
+
StoreSearchRequest,
|
|
20
|
+
StoreSearchResponse,
|
|
21
|
+
)
|
|
22
|
+
from aegra_api.models.threads import (
|
|
23
|
+
Thread,
|
|
24
|
+
ThreadCheckpoint,
|
|
25
|
+
ThreadCheckpointPostRequest,
|
|
26
|
+
ThreadCreate,
|
|
27
|
+
ThreadHistoryRequest,
|
|
28
|
+
ThreadList,
|
|
29
|
+
ThreadSearchRequest,
|
|
30
|
+
ThreadSearchResponse,
|
|
31
|
+
ThreadState,
|
|
32
|
+
ThreadStateUpdate,
|
|
33
|
+
ThreadStateUpdateResponse,
|
|
34
|
+
ThreadUpdate,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Assistants
|
|
39
|
+
"Assistant",
|
|
40
|
+
"AssistantCreate",
|
|
41
|
+
"AssistantList",
|
|
42
|
+
"AssistantSearchRequest",
|
|
43
|
+
"AssistantUpdate",
|
|
44
|
+
"AgentSchemas",
|
|
45
|
+
# Threads
|
|
46
|
+
"Thread",
|
|
47
|
+
"ThreadCreate",
|
|
48
|
+
"ThreadList",
|
|
49
|
+
"ThreadSearchRequest",
|
|
50
|
+
"ThreadSearchResponse",
|
|
51
|
+
"ThreadState",
|
|
52
|
+
"ThreadStateUpdate",
|
|
53
|
+
"ThreadStateUpdateResponse",
|
|
54
|
+
"ThreadCheckpoint",
|
|
55
|
+
"ThreadCheckpointPostRequest",
|
|
56
|
+
"ThreadHistoryRequest",
|
|
57
|
+
# Runs
|
|
58
|
+
"Run",
|
|
59
|
+
"RunCreate",
|
|
60
|
+
"RunStatus",
|
|
61
|
+
# Store
|
|
62
|
+
"StorePutRequest",
|
|
63
|
+
"StoreGetResponse",
|
|
64
|
+
"StoreSearchRequest",
|
|
65
|
+
"StoreSearchResponse",
|
|
66
|
+
"StoreItem",
|
|
67
|
+
"StoreDeleteRequest",
|
|
68
|
+
# Errors
|
|
69
|
+
"AgentProtocolError",
|
|
70
|
+
"get_error_type",
|
|
71
|
+
# Auth
|
|
72
|
+
"User",
|
|
73
|
+
"AuthContext",
|
|
74
|
+
"TokenPayload",
|
|
75
|
+
"ThreadUpdate",
|
|
76
|
+
]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Assistant-related Pydantic models for Agent Protocol"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AssistantCreate(BaseModel):
|
|
10
|
+
"""Request model for creating assistants"""
|
|
11
|
+
|
|
12
|
+
assistant_id: str | None = Field(None, description="Unique assistant identifier (auto-generated if not provided)")
|
|
13
|
+
name: str | None = Field(
|
|
14
|
+
None,
|
|
15
|
+
description="Human-readable assistant name (auto-generated if not provided)",
|
|
16
|
+
)
|
|
17
|
+
description: str | None = Field(None, description="Assistant description")
|
|
18
|
+
config: dict[str, Any] | None = Field({}, description="Assistant configuration")
|
|
19
|
+
context: dict[str, Any] | None = Field({}, description="Assistant context")
|
|
20
|
+
graph_id: str = Field(..., description="LangGraph graph ID from aegra.json")
|
|
21
|
+
metadata: dict[str, Any] | None = Field({}, description="Metadata to use for searching and filtering assistants.")
|
|
22
|
+
if_exists: str | None = Field("error", description="What to do if assistant exists: error or do_nothing")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Assistant(BaseModel):
|
|
26
|
+
"""Assistant entity model"""
|
|
27
|
+
|
|
28
|
+
assistant_id: str
|
|
29
|
+
name: str
|
|
30
|
+
description: str | None = None
|
|
31
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
32
|
+
context: dict[str, Any] = Field(default_factory=dict)
|
|
33
|
+
graph_id: str
|
|
34
|
+
user_id: str
|
|
35
|
+
version: int = Field(..., description="The version of the assistant.")
|
|
36
|
+
metadata: dict[str, Any] = Field(default_factory=dict, alias="metadata_dict")
|
|
37
|
+
created_at: datetime
|
|
38
|
+
updated_at: datetime
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AssistantUpdate(BaseModel):
|
|
44
|
+
"""Request model for creating assistants"""
|
|
45
|
+
|
|
46
|
+
name: str | None = Field(None, description="The name of the assistant (auto-generated if not provided)")
|
|
47
|
+
description: str | None = Field(None, description="The description of the assistant. Defaults to null.")
|
|
48
|
+
config: dict[str, Any] | None = Field({}, description="Configuration to use for the graph.")
|
|
49
|
+
graph_id: str = Field("agent", description="The ID of the graph")
|
|
50
|
+
context: dict[str, Any] | None = Field(
|
|
51
|
+
{},
|
|
52
|
+
description="The context to use for the graph. Useful when graph is configurable.",
|
|
53
|
+
)
|
|
54
|
+
metadata: dict[str, Any] | None = Field({}, description="Metadata to use for searching and filtering assistants.")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AssistantList(BaseModel):
|
|
58
|
+
"""Response model for listing assistants"""
|
|
59
|
+
|
|
60
|
+
assistants: list[Assistant]
|
|
61
|
+
total: int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class AssistantSearchRequest(BaseModel):
|
|
65
|
+
"""Request model for assistant search"""
|
|
66
|
+
|
|
67
|
+
name: str | None = Field(None, description="Filter by assistant name")
|
|
68
|
+
description: str | None = Field(None, description="Filter by assistant description")
|
|
69
|
+
graph_id: str | None = Field(None, description="Filter by graph ID")
|
|
70
|
+
limit: int | None = Field(20, le=100, ge=1, description="Maximum results")
|
|
71
|
+
offset: int | None = Field(0, ge=0, description="Results offset")
|
|
72
|
+
metadata: dict[str, Any] | None = Field({}, description="Metadata to use for searching and filtering assistants.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AgentSchemas(BaseModel):
|
|
76
|
+
"""Agent schema definitions for client integration"""
|
|
77
|
+
|
|
78
|
+
input_schema: dict[str, Any] = Field(..., description="JSON Schema for agent inputs")
|
|
79
|
+
output_schema: dict[str, Any] = Field(..., description="JSON Schema for agent outputs")
|
|
80
|
+
state_schema: dict[str, Any] = Field(..., description="JSON Schema for agent state")
|
|
81
|
+
config_schema: dict[str, Any] = Field(..., description="JSON Schema for agent config")
|
aegra_api/models/auth.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Authentication and user context models"""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class User(BaseModel):
|
|
9
|
+
"""User model that accepts any auth fields.
|
|
10
|
+
|
|
11
|
+
This model uses ConfigDict(extra="allow") to accept any additional fields
|
|
12
|
+
from auth handlers (e.g., subscription_tier, team_id) while maintaining
|
|
13
|
+
type hints for common fields.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
model_config = ConfigDict(extra="allow")
|
|
17
|
+
|
|
18
|
+
# Required
|
|
19
|
+
identity: str
|
|
20
|
+
|
|
21
|
+
# Optional with defaults
|
|
22
|
+
is_authenticated: bool = True
|
|
23
|
+
permissions: list[str] = []
|
|
24
|
+
display_name: str | None = None
|
|
25
|
+
|
|
26
|
+
# Common optional fields (for IDE hints)
|
|
27
|
+
org_id: str | None = None
|
|
28
|
+
email: str | None = None
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict[str, Any]:
|
|
31
|
+
"""Convert to dict including all extra fields."""
|
|
32
|
+
return self.model_dump()
|
|
33
|
+
|
|
34
|
+
def __getattr__(self, name: str) -> Any:
|
|
35
|
+
"""Allow attribute access to extra fields."""
|
|
36
|
+
try:
|
|
37
|
+
extra = object.__getattribute__(self, "__pydantic_extra__") or {}
|
|
38
|
+
except AttributeError:
|
|
39
|
+
extra = {}
|
|
40
|
+
if name in extra:
|
|
41
|
+
return extra[name]
|
|
42
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AuthContext(BaseModel):
|
|
46
|
+
"""Authentication context for request processing"""
|
|
47
|
+
|
|
48
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
49
|
+
|
|
50
|
+
user: User
|
|
51
|
+
request_id: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TokenPayload(BaseModel):
|
|
55
|
+
"""JWT token payload structure"""
|
|
56
|
+
|
|
57
|
+
sub: str # subject (user ID)
|
|
58
|
+
name: str | None = None
|
|
59
|
+
scopes: list[str] = []
|
|
60
|
+
org: str | None = None
|
|
61
|
+
exp: int | None = None
|
|
62
|
+
iat: int | None = None
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Status enums for Aegra API specification."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
# Run status enum
|
|
6
|
+
RunStatus = Literal[
|
|
7
|
+
"pending",
|
|
8
|
+
"running",
|
|
9
|
+
"error",
|
|
10
|
+
"success",
|
|
11
|
+
"timeout",
|
|
12
|
+
"interrupted",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# Thread status enum
|
|
16
|
+
ThreadStatus = Literal[
|
|
17
|
+
"idle",
|
|
18
|
+
"busy",
|
|
19
|
+
"interrupted",
|
|
20
|
+
"error",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Multitask strategy enum
|
|
24
|
+
MultitaskStrategy = Literal[
|
|
25
|
+
"reject",
|
|
26
|
+
"rollback",
|
|
27
|
+
"interrupt",
|
|
28
|
+
"enqueue",
|
|
29
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Error response models for Agent Protocol"""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentProtocolError(BaseModel):
|
|
9
|
+
"""Standard Agent Protocol error response"""
|
|
10
|
+
|
|
11
|
+
error: str = Field(..., description="Error type")
|
|
12
|
+
message: str = Field(..., description="Human-readable error message")
|
|
13
|
+
details: dict[str, Any] | None = Field(None, description="Additional error details")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_error_type(status_code: int) -> str:
|
|
17
|
+
"""Map HTTP status codes to error types"""
|
|
18
|
+
error_map = {
|
|
19
|
+
400: "bad_request",
|
|
20
|
+
401: "unauthorized",
|
|
21
|
+
403: "forbidden",
|
|
22
|
+
404: "not_found",
|
|
23
|
+
409: "conflict",
|
|
24
|
+
422: "validation_error",
|
|
25
|
+
500: "internal_error",
|
|
26
|
+
501: "not_implemented",
|
|
27
|
+
503: "service_unavailable",
|
|
28
|
+
}
|
|
29
|
+
return error_map.get(status_code, "unknown_error")
|
aegra_api/models/runs.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Run-related Pydantic models for Agent Protocol"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any, Self
|
|
5
|
+
|
|
6
|
+
from pydantic import (
|
|
7
|
+
BaseModel,
|
|
8
|
+
ConfigDict,
|
|
9
|
+
Field,
|
|
10
|
+
field_validator,
|
|
11
|
+
model_validator,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from aegra_api.utils.status_compat import validate_run_status
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RunCreate(BaseModel):
|
|
18
|
+
"""Request model for creating runs"""
|
|
19
|
+
|
|
20
|
+
assistant_id: str = Field(..., description="Assistant to execute")
|
|
21
|
+
input: dict[str, Any] | None = Field(
|
|
22
|
+
None,
|
|
23
|
+
description="Input data for the run. Optional when resuming from a checkpoint.",
|
|
24
|
+
)
|
|
25
|
+
config: dict[str, Any] | None = Field({}, description="Execution config")
|
|
26
|
+
context: dict[str, Any] | None = Field({}, description="Execution context")
|
|
27
|
+
checkpoint: dict[str, Any] | None = Field(
|
|
28
|
+
None,
|
|
29
|
+
description="Checkpoint configuration (e.g., {'checkpoint_id': '...', 'checkpoint_ns': ''})",
|
|
30
|
+
)
|
|
31
|
+
stream: bool = Field(False, description="Enable streaming response")
|
|
32
|
+
stream_mode: str | list[str] | None = Field(None, description="Requested stream mode(s)")
|
|
33
|
+
on_disconnect: str | None = Field(
|
|
34
|
+
None,
|
|
35
|
+
description="Behavior on client disconnect: 'cancel' (default) or 'continue'.",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
multitask_strategy: str | None = Field(
|
|
39
|
+
None,
|
|
40
|
+
description="Strategy for handling concurrent runs on same thread: 'reject', 'interrupt', 'rollback', or 'enqueue'.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Human-in-the-loop fields (core HITL functionality)
|
|
44
|
+
command: dict[str, Any] | None = Field(
|
|
45
|
+
None,
|
|
46
|
+
description="Command for resuming interrupted runs with state updates or navigation",
|
|
47
|
+
)
|
|
48
|
+
interrupt_before: str | list[str] | None = Field(
|
|
49
|
+
None,
|
|
50
|
+
description="Nodes to interrupt immediately before they get executed. Use '*' for all nodes.",
|
|
51
|
+
)
|
|
52
|
+
interrupt_after: str | list[str] | None = Field(
|
|
53
|
+
None,
|
|
54
|
+
description="Nodes to interrupt immediately after they get executed. Use '*' for all nodes.",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Subgraph configuration
|
|
58
|
+
stream_subgraphs: bool | None = Field(
|
|
59
|
+
False,
|
|
60
|
+
description="Whether to include subgraph events in streaming. When True, includes events from all subgraphs. When False (default when None), excludes subgraph events. Defaults to False for backwards compatibility.",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Request metadata (top-level in payload)
|
|
64
|
+
metadata: dict[str, Any] | None = Field(
|
|
65
|
+
None,
|
|
66
|
+
description="Request metadata (e.g., from_studio flag)",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@model_validator(mode="after")
|
|
70
|
+
def validate_input_command_exclusivity(self) -> Self:
|
|
71
|
+
"""Ensure input and command are mutually exclusive"""
|
|
72
|
+
# Allow empty input dict when command is present (frontend compatibility)
|
|
73
|
+
if self.input is not None and self.command is not None:
|
|
74
|
+
# If input is just an empty dict, treat it as None for compatibility
|
|
75
|
+
if self.input == {}:
|
|
76
|
+
self.input = None
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError("Cannot specify both 'input' and 'command' - they are mutually exclusive")
|
|
79
|
+
if self.input is None and self.command is None:
|
|
80
|
+
if self.checkpoint is not None:
|
|
81
|
+
# Allow checkpoint-only requests by treating input as empty dict
|
|
82
|
+
self.input = {}
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError("Must specify either 'input' or 'command'")
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Run(BaseModel):
|
|
89
|
+
"""Run entity model
|
|
90
|
+
|
|
91
|
+
Status values: pending, running, error, success, timeout, interrupted
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(from_attributes=True)
|
|
95
|
+
|
|
96
|
+
run_id: str
|
|
97
|
+
thread_id: str
|
|
98
|
+
assistant_id: str
|
|
99
|
+
status: str = "pending" # Valid values: pending, running, error, success, timeout, interrupted
|
|
100
|
+
input: dict[str, Any]
|
|
101
|
+
output: dict[str, Any] | None = None
|
|
102
|
+
error_message: str | None = None
|
|
103
|
+
config: dict[str, Any] | None = {}
|
|
104
|
+
context: dict[str, Any] | None = {}
|
|
105
|
+
user_id: str
|
|
106
|
+
created_at: datetime
|
|
107
|
+
updated_at: datetime
|
|
108
|
+
|
|
109
|
+
@field_validator("status", mode="before")
|
|
110
|
+
@classmethod
|
|
111
|
+
def validate_status(cls, v: str) -> str:
|
|
112
|
+
"""Validate status conforms to API specification."""
|
|
113
|
+
if not isinstance(v, str):
|
|
114
|
+
raise ValueError(f"Status must be a string, got {type(v)}")
|
|
115
|
+
return validate_run_status(v)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class RunStatus(BaseModel):
|
|
119
|
+
"""Simple run status response"""
|
|
120
|
+
|
|
121
|
+
run_id: str
|
|
122
|
+
status: str # Standard status value
|
|
123
|
+
|
|
124
|
+
message: str | None = None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Store-related Pydantic models for Agent Protocol"""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StorePutRequest(BaseModel):
|
|
9
|
+
"""Request model for storing items"""
|
|
10
|
+
|
|
11
|
+
namespace: list[str] = Field(..., description="Storage namespace")
|
|
12
|
+
key: str = Field(..., description="Item key")
|
|
13
|
+
value: dict[str, Any] = Field(..., description="Item value (must be a JSON object)")
|
|
14
|
+
|
|
15
|
+
@field_validator("value", mode="before")
|
|
16
|
+
@classmethod
|
|
17
|
+
def validate_value_is_dict(cls, v: Any) -> dict[str, Any]:
|
|
18
|
+
"""Validate that value is a dictionary.
|
|
19
|
+
|
|
20
|
+
LangGraph store requires values to be dictionaries for proper
|
|
21
|
+
serialization and search functionality.
|
|
22
|
+
"""
|
|
23
|
+
if not isinstance(v, dict):
|
|
24
|
+
raise ValueError(f"Value must be a dictionary (JSON object), got {type(v).__name__}")
|
|
25
|
+
return v
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class StoreGetResponse(BaseModel):
|
|
29
|
+
"""Response model for getting items"""
|
|
30
|
+
|
|
31
|
+
key: str
|
|
32
|
+
value: Any
|
|
33
|
+
namespace: list[str]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class StoreSearchRequest(BaseModel):
|
|
37
|
+
"""Request model for searching store items"""
|
|
38
|
+
|
|
39
|
+
namespace_prefix: list[str] = Field(..., description="Namespace prefix to search")
|
|
40
|
+
filter: dict[str, Any] | None = Field(None, description="Optional dictionary of key-value pairs to filter results.")
|
|
41
|
+
query: str | None = Field(None, description="Search query")
|
|
42
|
+
limit: int | None = Field(20, le=100, ge=1, description="Maximum results")
|
|
43
|
+
offset: int | None = Field(0, ge=0, description="Results offset")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StoreItem(BaseModel):
|
|
47
|
+
"""Store item model"""
|
|
48
|
+
|
|
49
|
+
key: str
|
|
50
|
+
value: Any
|
|
51
|
+
namespace: list[str]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StoreSearchResponse(BaseModel):
|
|
55
|
+
"""Response model for store search"""
|
|
56
|
+
|
|
57
|
+
items: list[StoreItem]
|
|
58
|
+
total: int
|
|
59
|
+
limit: int
|
|
60
|
+
offset: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class StoreDeleteRequest(BaseModel):
|
|
64
|
+
"""Request body for deleting store items (SDK-compatible)."""
|
|
65
|
+
|
|
66
|
+
namespace: list[str]
|
|
67
|
+
key: str
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Thread-related Pydantic models for Agent Protocol"""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
7
|
+
|
|
8
|
+
from aegra_api.utils.status_compat import validate_thread_status
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ThreadCreate(BaseModel):
|
|
12
|
+
"""Request model for creating threads"""
|
|
13
|
+
|
|
14
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
15
|
+
|
|
16
|
+
metadata: dict[str, Any] | None = Field(None, description="Thread metadata")
|
|
17
|
+
initial_state: dict[str, Any] | None = Field(None, description="LangGraph initial state")
|
|
18
|
+
thread_id: str | None = Field(
|
|
19
|
+
None,
|
|
20
|
+
alias="threadId",
|
|
21
|
+
description="Optional client-provided thread ID for idempotent creation",
|
|
22
|
+
)
|
|
23
|
+
if_exists: str | None = Field(
|
|
24
|
+
"raise",
|
|
25
|
+
alias="ifExists",
|
|
26
|
+
description="Behavior when thread exists: 'raise' (default) or 'do_nothing'",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ThreadUpdate(BaseModel):
|
|
31
|
+
"""Request model for updating threads"""
|
|
32
|
+
|
|
33
|
+
metadata: dict[str, Any] | None = Field(None, description="Thread metadata to update")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Thread(BaseModel):
|
|
37
|
+
"""Thread entity model
|
|
38
|
+
|
|
39
|
+
Status values: idle, busy, interrupted, error
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(from_attributes=True)
|
|
43
|
+
|
|
44
|
+
thread_id: str
|
|
45
|
+
status: str = "idle" # Valid values: idle, busy, interrupted, error
|
|
46
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
47
|
+
user_id: str
|
|
48
|
+
created_at: datetime
|
|
49
|
+
updated_at: datetime
|
|
50
|
+
|
|
51
|
+
@field_validator("status", mode="before")
|
|
52
|
+
@classmethod
|
|
53
|
+
def validate_status(cls, v: str) -> str:
|
|
54
|
+
"""Validate status conforms to API specification."""
|
|
55
|
+
if not isinstance(v, str):
|
|
56
|
+
raise ValueError(f"Status must be a string, got {type(v)}")
|
|
57
|
+
return validate_thread_status(v)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ThreadList(BaseModel):
|
|
61
|
+
"""Response model for listing threads"""
|
|
62
|
+
|
|
63
|
+
threads: list[Thread]
|
|
64
|
+
total: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ThreadSearchRequest(BaseModel):
|
|
68
|
+
"""Request model for thread search"""
|
|
69
|
+
|
|
70
|
+
metadata: dict[str, Any] | None = Field(None, description="Metadata filters")
|
|
71
|
+
status: str | None = Field(None, description="Thread status filter (idle, busy, interrupted, error)")
|
|
72
|
+
limit: int | None = Field(20, le=100, ge=1, description="Maximum results")
|
|
73
|
+
offset: int | None = Field(0, ge=0, description="Results offset")
|
|
74
|
+
order_by: str | None = Field("created_at DESC", description="Sort order")
|
|
75
|
+
|
|
76
|
+
@field_validator("status")
|
|
77
|
+
@classmethod
|
|
78
|
+
def validate_status(cls, v: str | None) -> str | None:
|
|
79
|
+
"""Validate status filter conforms to API specification."""
|
|
80
|
+
if v is not None:
|
|
81
|
+
return validate_thread_status(v)
|
|
82
|
+
return v
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ThreadSearchResponse(BaseModel):
|
|
86
|
+
"""Response model for thread search"""
|
|
87
|
+
|
|
88
|
+
threads: list[Thread]
|
|
89
|
+
total: int
|
|
90
|
+
limit: int
|
|
91
|
+
offset: int
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ThreadCheckpoint(BaseModel):
|
|
95
|
+
"""Checkpoint identifier for thread history"""
|
|
96
|
+
|
|
97
|
+
checkpoint_id: str | None = None
|
|
98
|
+
thread_id: str | None = None
|
|
99
|
+
checkpoint_ns: str | None = ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ThreadCheckpointPostRequest(BaseModel):
|
|
103
|
+
"""Request model for fetching thread checkpoint"""
|
|
104
|
+
|
|
105
|
+
checkpoint: ThreadCheckpoint = Field(description="Checkpoint to fetch")
|
|
106
|
+
subgraphs: bool | None = Field(False, description="Include subgraph states")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ThreadState(BaseModel):
|
|
110
|
+
"""Thread state model for history endpoint"""
|
|
111
|
+
|
|
112
|
+
values: dict[str, Any] = Field(description="Channel values (messages, etc.)")
|
|
113
|
+
next: list[str] = Field(default_factory=list, description="Next nodes to execute")
|
|
114
|
+
tasks: list[dict[str, Any]] = Field(default_factory=list, description="Tasks to execute")
|
|
115
|
+
interrupts: list[dict[str, Any]] = Field(default_factory=list, description="Interrupt data")
|
|
116
|
+
metadata: dict[str, Any] = Field(default_factory=dict, description="Checkpoint metadata")
|
|
117
|
+
created_at: datetime | None = Field(None, description="Timestamp of state creation")
|
|
118
|
+
checkpoint: ThreadCheckpoint = Field(description="Current checkpoint")
|
|
119
|
+
parent_checkpoint: ThreadCheckpoint | None = Field(None, description="Parent checkpoint")
|
|
120
|
+
checkpoint_id: str | None = Field(None, description="Checkpoint ID (for backward compatibility)")
|
|
121
|
+
parent_checkpoint_id: str | None = Field(None, description="Parent checkpoint ID (for backward compatibility)")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ThreadStateUpdate(BaseModel):
|
|
125
|
+
"""Request model for updating thread state"""
|
|
126
|
+
|
|
127
|
+
values: dict[str, Any] | list[dict[str, Any]] | None = Field(
|
|
128
|
+
None, description="The values to update the state with"
|
|
129
|
+
)
|
|
130
|
+
checkpoint: dict[str, Any] | None = Field(None, description="The checkpoint to update the state of")
|
|
131
|
+
checkpoint_id: str | None = Field(None, description="Optional checkpoint ID to update from")
|
|
132
|
+
as_node: str | None = Field(None, description="Update the state as if this node had just executed")
|
|
133
|
+
# Also support query-like parameters for GET-like behavior via POST
|
|
134
|
+
subgraphs: bool | None = Field(False, description="Include states from subgraphs")
|
|
135
|
+
checkpoint_ns: str | None = Field(None, description="Checkpoint namespace")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ThreadStateUpdateResponse(BaseModel):
|
|
139
|
+
"""Response model for thread state update"""
|
|
140
|
+
|
|
141
|
+
checkpoint: dict[str, Any] = Field(description="The checkpoint that was created/updated")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ThreadHistoryRequest(BaseModel):
|
|
145
|
+
"""Request model for thread history endpoint"""
|
|
146
|
+
|
|
147
|
+
limit: int | None = Field(10, ge=1, le=1000, description="Number of states to return")
|
|
148
|
+
before: str | None = Field(None, description="Return states before this checkpoint ID")
|
|
149
|
+
metadata: dict[str, Any] | None = Field(None, description="Filter by metadata")
|
|
150
|
+
checkpoint: dict[str, Any] | None = Field(None, description="Checkpoint for subgraph filtering")
|
|
151
|
+
subgraphs: bool | None = Field(False, description="Include states from subgraphs")
|
|
152
|
+
checkpoint_ns: str | None = Field(None, description="Checkpoint namespace")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Observability module for the agent server."""
|