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.
Files changed (64) hide show
  1. aegra_api/__init__.py +3 -0
  2. aegra_api/api/__init__.py +1 -0
  3. aegra_api/api/assistants.py +235 -0
  4. aegra_api/api/runs.py +1110 -0
  5. aegra_api/api/store.py +200 -0
  6. aegra_api/api/threads.py +761 -0
  7. aegra_api/config.py +204 -0
  8. aegra_api/constants.py +5 -0
  9. aegra_api/core/__init__.py +0 -0
  10. aegra_api/core/app_loader.py +91 -0
  11. aegra_api/core/auth_ctx.py +65 -0
  12. aegra_api/core/auth_deps.py +186 -0
  13. aegra_api/core/auth_handlers.py +248 -0
  14. aegra_api/core/auth_middleware.py +331 -0
  15. aegra_api/core/database.py +123 -0
  16. aegra_api/core/health.py +131 -0
  17. aegra_api/core/orm.py +165 -0
  18. aegra_api/core/route_merger.py +69 -0
  19. aegra_api/core/serializers/__init__.py +7 -0
  20. aegra_api/core/serializers/base.py +22 -0
  21. aegra_api/core/serializers/general.py +54 -0
  22. aegra_api/core/serializers/langgraph.py +102 -0
  23. aegra_api/core/sse.py +178 -0
  24. aegra_api/main.py +303 -0
  25. aegra_api/middleware/__init__.py +4 -0
  26. aegra_api/middleware/double_encoded_json.py +74 -0
  27. aegra_api/middleware/logger_middleware.py +95 -0
  28. aegra_api/models/__init__.py +76 -0
  29. aegra_api/models/assistants.py +81 -0
  30. aegra_api/models/auth.py +62 -0
  31. aegra_api/models/enums.py +29 -0
  32. aegra_api/models/errors.py +29 -0
  33. aegra_api/models/runs.py +124 -0
  34. aegra_api/models/store.py +67 -0
  35. aegra_api/models/threads.py +152 -0
  36. aegra_api/observability/__init__.py +1 -0
  37. aegra_api/observability/base.py +88 -0
  38. aegra_api/observability/otel.py +133 -0
  39. aegra_api/observability/setup.py +27 -0
  40. aegra_api/observability/targets/__init__.py +11 -0
  41. aegra_api/observability/targets/base.py +18 -0
  42. aegra_api/observability/targets/langfuse.py +33 -0
  43. aegra_api/observability/targets/otlp.py +38 -0
  44. aegra_api/observability/targets/phoenix.py +24 -0
  45. aegra_api/services/__init__.py +0 -0
  46. aegra_api/services/assistant_service.py +569 -0
  47. aegra_api/services/base_broker.py +59 -0
  48. aegra_api/services/broker.py +141 -0
  49. aegra_api/services/event_converter.py +157 -0
  50. aegra_api/services/event_store.py +196 -0
  51. aegra_api/services/graph_streaming.py +433 -0
  52. aegra_api/services/langgraph_service.py +456 -0
  53. aegra_api/services/streaming_service.py +362 -0
  54. aegra_api/services/thread_state_service.py +128 -0
  55. aegra_api/settings.py +124 -0
  56. aegra_api/utils/__init__.py +3 -0
  57. aegra_api/utils/assistants.py +23 -0
  58. aegra_api/utils/run_utils.py +60 -0
  59. aegra_api/utils/setup_logging.py +122 -0
  60. aegra_api/utils/sse_utils.py +26 -0
  61. aegra_api/utils/status_compat.py +57 -0
  62. aegra_api-0.1.0.dist-info/METADATA +244 -0
  63. aegra_api-0.1.0.dist-info/RECORD +64 -0
  64. 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