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
aegra_api/api/store.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Store endpoints for Agent Protocol"""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
4
|
+
|
|
5
|
+
from aegra_api.core.auth_deps import get_current_user
|
|
6
|
+
from aegra_api.core.auth_handlers import build_auth_context, handle_event
|
|
7
|
+
from aegra_api.models import (
|
|
8
|
+
StoreDeleteRequest,
|
|
9
|
+
StoreGetResponse,
|
|
10
|
+
StoreItem,
|
|
11
|
+
StorePutRequest,
|
|
12
|
+
StoreSearchRequest,
|
|
13
|
+
StoreSearchResponse,
|
|
14
|
+
User,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
router = APIRouter(tags=["Store"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.put("/store/items")
|
|
21
|
+
async def put_store_item(request: StorePutRequest, user: User = Depends(get_current_user)):
|
|
22
|
+
"""Store an item in the LangGraph store"""
|
|
23
|
+
# Authorization check
|
|
24
|
+
ctx = build_auth_context(user, "store", "put")
|
|
25
|
+
value = request.model_dump()
|
|
26
|
+
filters = await handle_event(ctx, value)
|
|
27
|
+
|
|
28
|
+
# If handler modified namespace/key/value, update request
|
|
29
|
+
if filters:
|
|
30
|
+
if "namespace" in filters:
|
|
31
|
+
request.namespace = filters["namespace"]
|
|
32
|
+
if "key" in filters:
|
|
33
|
+
request.key = filters["key"]
|
|
34
|
+
if "value" in filters:
|
|
35
|
+
request.value = filters["value"]
|
|
36
|
+
|
|
37
|
+
# Apply user namespace scoping
|
|
38
|
+
scoped_namespace = apply_user_namespace_scoping(user.identity, request.namespace)
|
|
39
|
+
|
|
40
|
+
# Get LangGraph store from database manager
|
|
41
|
+
from aegra_api.core.database import db_manager
|
|
42
|
+
|
|
43
|
+
store = db_manager.get_store()
|
|
44
|
+
|
|
45
|
+
await store.aput(namespace=tuple(scoped_namespace), key=request.key, value=request.value)
|
|
46
|
+
|
|
47
|
+
return {"status": "stored"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@router.get("/store/items", response_model=StoreGetResponse)
|
|
51
|
+
async def get_store_item(
|
|
52
|
+
key: str,
|
|
53
|
+
namespace: str | list[str] | None = Query(None),
|
|
54
|
+
user: User = Depends(get_current_user),
|
|
55
|
+
):
|
|
56
|
+
"""Get an item from the LangGraph store"""
|
|
57
|
+
# Authorization check
|
|
58
|
+
ctx = build_auth_context(user, "store", "get")
|
|
59
|
+
value = {"key": key, "namespace": namespace}
|
|
60
|
+
filters = await handle_event(ctx, value)
|
|
61
|
+
|
|
62
|
+
# If handler modified namespace/key, update
|
|
63
|
+
if filters:
|
|
64
|
+
if "namespace" in filters:
|
|
65
|
+
namespace = filters["namespace"]
|
|
66
|
+
if "key" in filters:
|
|
67
|
+
key = filters["key"]
|
|
68
|
+
|
|
69
|
+
# Accept SDK-style dotted namespaces or list
|
|
70
|
+
ns_list: list[str]
|
|
71
|
+
if isinstance(namespace, str):
|
|
72
|
+
ns_list = [part for part in namespace.split(".") if part]
|
|
73
|
+
elif isinstance(namespace, list):
|
|
74
|
+
ns_list = namespace
|
|
75
|
+
else:
|
|
76
|
+
ns_list = []
|
|
77
|
+
|
|
78
|
+
# Apply user namespace scoping
|
|
79
|
+
scoped_namespace = apply_user_namespace_scoping(user.identity, ns_list)
|
|
80
|
+
|
|
81
|
+
# Get LangGraph store from database manager
|
|
82
|
+
from aegra_api.core.database import db_manager
|
|
83
|
+
|
|
84
|
+
store = db_manager.get_store()
|
|
85
|
+
|
|
86
|
+
item = await store.aget(tuple(scoped_namespace), key)
|
|
87
|
+
|
|
88
|
+
if not item:
|
|
89
|
+
raise HTTPException(404, "Item not found")
|
|
90
|
+
|
|
91
|
+
return StoreGetResponse(key=key, value=item.value, namespace=list(scoped_namespace))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.delete("/store/items")
|
|
95
|
+
async def delete_store_item(
|
|
96
|
+
body: StoreDeleteRequest | None = None,
|
|
97
|
+
key: str | None = Query(None),
|
|
98
|
+
namespace: list[str] | None = Query(None),
|
|
99
|
+
user: User = Depends(get_current_user),
|
|
100
|
+
):
|
|
101
|
+
"""Delete an item from the LangGraph store.
|
|
102
|
+
|
|
103
|
+
Compatible with SDK which sends JSON body {namespace, key}.
|
|
104
|
+
Also accepts query params for manual usage.
|
|
105
|
+
"""
|
|
106
|
+
# Determine source of parameters
|
|
107
|
+
ns = None
|
|
108
|
+
k = None
|
|
109
|
+
if body is not None:
|
|
110
|
+
ns = body.namespace
|
|
111
|
+
k = body.key
|
|
112
|
+
else:
|
|
113
|
+
if key is None:
|
|
114
|
+
raise HTTPException(422, "Missing 'key' parameter")
|
|
115
|
+
ns = namespace or []
|
|
116
|
+
k = key
|
|
117
|
+
|
|
118
|
+
# Authorization check
|
|
119
|
+
ctx = build_auth_context(user, "store", "delete")
|
|
120
|
+
value = {"namespace": ns, "key": k}
|
|
121
|
+
filters = await handle_event(ctx, value)
|
|
122
|
+
|
|
123
|
+
# If handler modified namespace/key, update
|
|
124
|
+
if filters:
|
|
125
|
+
if "namespace" in filters:
|
|
126
|
+
ns = filters["namespace"]
|
|
127
|
+
if "key" in filters:
|
|
128
|
+
k = filters["key"]
|
|
129
|
+
|
|
130
|
+
# Apply user namespace scoping
|
|
131
|
+
scoped_namespace = apply_user_namespace_scoping(user.identity, ns)
|
|
132
|
+
|
|
133
|
+
# Get LangGraph store from database manager
|
|
134
|
+
from aegra_api.core.database import db_manager
|
|
135
|
+
|
|
136
|
+
store = db_manager.get_store()
|
|
137
|
+
|
|
138
|
+
await store.adelete(tuple(scoped_namespace), k)
|
|
139
|
+
|
|
140
|
+
return {"status": "deleted"}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@router.post("/store/items/search", response_model=StoreSearchResponse)
|
|
144
|
+
async def search_store_items(request: StoreSearchRequest, user: User = Depends(get_current_user)):
|
|
145
|
+
"""Search items in the LangGraph store"""
|
|
146
|
+
# Authorization check
|
|
147
|
+
ctx = build_auth_context(user, "store", "search")
|
|
148
|
+
value = request.model_dump()
|
|
149
|
+
filters = await handle_event(ctx, value)
|
|
150
|
+
|
|
151
|
+
# Merge handler filters with request filters
|
|
152
|
+
if filters:
|
|
153
|
+
if "namespace_prefix" in filters:
|
|
154
|
+
request.namespace_prefix = filters["namespace_prefix"]
|
|
155
|
+
|
|
156
|
+
handler_filters = {k: v for k, v in filters.items() if k != "namespace_prefix"}
|
|
157
|
+
if handler_filters:
|
|
158
|
+
request.filter = {**(request.filter or {}), **handler_filters}
|
|
159
|
+
|
|
160
|
+
# Apply user namespace scoping
|
|
161
|
+
scoped_prefix = apply_user_namespace_scoping(user.identity, request.namespace_prefix)
|
|
162
|
+
|
|
163
|
+
# Get LangGraph store from database manager
|
|
164
|
+
from aegra_api.core.database import db_manager
|
|
165
|
+
|
|
166
|
+
store = db_manager.get_store()
|
|
167
|
+
|
|
168
|
+
# Search with LangGraph store
|
|
169
|
+
# asearch takes namespace_prefix as a positional-only argument
|
|
170
|
+
results = await store.asearch(
|
|
171
|
+
tuple(scoped_prefix),
|
|
172
|
+
query=request.query,
|
|
173
|
+
filter=request.filter,
|
|
174
|
+
limit=request.limit or 20,
|
|
175
|
+
offset=request.offset or 0,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
items = [StoreItem(key=r.key, value=r.value, namespace=list(r.namespace)) for r in results]
|
|
179
|
+
|
|
180
|
+
return StoreSearchResponse(
|
|
181
|
+
items=items,
|
|
182
|
+
total=len(items), # LangGraph store doesn't provide total count
|
|
183
|
+
limit=request.limit or 20,
|
|
184
|
+
offset=request.offset or 0,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def apply_user_namespace_scoping(user_id: str, namespace: list[str]) -> list[str]:
|
|
189
|
+
"""Apply user-based namespace scoping for data isolation"""
|
|
190
|
+
|
|
191
|
+
if not namespace:
|
|
192
|
+
# Default to user's private namespace
|
|
193
|
+
return ["users", user_id]
|
|
194
|
+
|
|
195
|
+
# Allow explicit user namespaces
|
|
196
|
+
if namespace[0] == "users" and len(namespace) >= 2 and namespace[1] == user_id:
|
|
197
|
+
return namespace
|
|
198
|
+
|
|
199
|
+
# For development, allow all namespaces (remove this for production)
|
|
200
|
+
return namespace
|