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/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Aegra API - Self-hosted Agent Protocol server."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """Agent Protocol API endpoints"""
@@ -0,0 +1,235 @@
1
+ """Assistant endpoints for Agent Protocol
2
+
3
+ NOTE: This API follows a layered architecture pattern with business logic
4
+ separated into a service layer (assistant_service.py). This was the first
5
+ API to be refactored, and the plan is to gradually refactor all other APIs
6
+ (runs, threads, etc.) to follow this same pattern for better code
7
+ organization, testability, and maintainability.
8
+
9
+ Architecture:
10
+ - API Layer (this file): Thin FastAPI route handlers, request/response handling
11
+ - Service Layer (assistant_service.py): Business logic, validation, orchestration
12
+ """
13
+
14
+ from fastapi import APIRouter, Body, Depends
15
+
16
+ from aegra_api.core.auth_deps import get_current_user
17
+ from aegra_api.core.auth_handlers import build_auth_context, handle_event
18
+ from aegra_api.models import (
19
+ Assistant,
20
+ AssistantCreate,
21
+ AssistantList,
22
+ AssistantSearchRequest,
23
+ AssistantUpdate,
24
+ User,
25
+ )
26
+ from aegra_api.services.assistant_service import AssistantService, get_assistant_service
27
+
28
+ router = APIRouter(tags=["Assistants"])
29
+
30
+
31
+ @router.post("/assistants", response_model=Assistant, response_model_by_alias=False)
32
+ async def create_assistant(
33
+ request: AssistantCreate,
34
+ user: User = Depends(get_current_user),
35
+ service: AssistantService = Depends(get_assistant_service),
36
+ ):
37
+ """Create a new assistant"""
38
+ # Authorization check
39
+ ctx = build_auth_context(user, "assistants", "create")
40
+ value = request.model_dump()
41
+ filters = await handle_event(ctx, value)
42
+
43
+ # If handler modified metadata, update request
44
+ if filters and "metadata" in filters:
45
+ request.metadata = {**(request.metadata or {}), **filters["metadata"]}
46
+ elif value.get("metadata"):
47
+ request.metadata = {**(request.metadata or {}), **value["metadata"]}
48
+
49
+ return await service.create_assistant(request, user.identity)
50
+
51
+
52
+ @router.get("/assistants", response_model=AssistantList, response_model_by_alias=False)
53
+ async def list_assistants(
54
+ user: User = Depends(get_current_user),
55
+ service: AssistantService = Depends(get_assistant_service),
56
+ ):
57
+ """List user's assistants"""
58
+ # Authorization check (search action for listing)
59
+ ctx = build_auth_context(user, "assistants", "search")
60
+ value = {}
61
+ filters = await handle_event(ctx, value)
62
+
63
+ # Apply filters if provided by handler
64
+ if filters:
65
+ # Convert filters to search request format
66
+ search_request = AssistantSearchRequest(filters=filters)
67
+ assistants = await service.search_assistants(search_request, user.identity)
68
+ else:
69
+ assistants = await service.list_assistants(user.identity)
70
+
71
+ return AssistantList(assistants=assistants, total=len(assistants))
72
+
73
+
74
+ @router.post("/assistants/search", response_model=list[Assistant], response_model_by_alias=False)
75
+ async def search_assistants(
76
+ request: AssistantSearchRequest,
77
+ user: User = Depends(get_current_user),
78
+ service: AssistantService = Depends(get_assistant_service),
79
+ ):
80
+ """Search assistants with filters"""
81
+ # Authorization check
82
+ ctx = build_auth_context(user, "assistants", "search")
83
+ value = request.model_dump()
84
+ filters = await handle_event(ctx, value)
85
+
86
+ # Merge handler filters with request filters
87
+ if filters:
88
+ request_filters = request.filters or {}
89
+ request.filters = {**request_filters, **filters}
90
+
91
+ return await service.search_assistants(request, user.identity)
92
+
93
+
94
+ @router.post("/assistants/count", response_model=int)
95
+ async def count_assistants(
96
+ request: AssistantSearchRequest,
97
+ user: User = Depends(get_current_user),
98
+ service: AssistantService = Depends(get_assistant_service),
99
+ ):
100
+ """Count assistants with filters"""
101
+ # Authorization check (search action for counting)
102
+ ctx = build_auth_context(user, "assistants", "search")
103
+ value = request.model_dump()
104
+ filters = await handle_event(ctx, value)
105
+
106
+ # Merge handler filters with request filters
107
+ if filters:
108
+ request_filters = request.filters or {}
109
+ request.filters = {**request_filters, **filters}
110
+
111
+ return await service.count_assistants(request, user.identity)
112
+
113
+
114
+ @router.get(
115
+ "/assistants/{assistant_id}",
116
+ response_model=Assistant,
117
+ response_model_by_alias=False,
118
+ )
119
+ async def get_assistant(
120
+ assistant_id: str,
121
+ user: User = Depends(get_current_user),
122
+ service: AssistantService = Depends(get_assistant_service),
123
+ ):
124
+ """Get assistant by ID"""
125
+ # Authorization check
126
+ ctx = build_auth_context(user, "assistants", "read")
127
+ value = {"assistant_id": assistant_id}
128
+ await handle_event(ctx, value)
129
+
130
+ return await service.get_assistant(assistant_id, user.identity)
131
+
132
+
133
+ @router.patch(
134
+ "/assistants/{assistant_id}",
135
+ response_model=Assistant,
136
+ response_model_by_alias=False,
137
+ )
138
+ async def update_assistant(
139
+ assistant_id: str,
140
+ request: AssistantUpdate,
141
+ user: User = Depends(get_current_user),
142
+ service: AssistantService = Depends(get_assistant_service),
143
+ ):
144
+ """Update assistant by ID"""
145
+ # Authorization check
146
+ ctx = build_auth_context(user, "assistants", "update")
147
+ value = {**request.model_dump(), "assistant_id": assistant_id}
148
+ filters = await handle_event(ctx, value)
149
+
150
+ # If handler modified metadata, update request
151
+ if filters and "metadata" in filters:
152
+ request.metadata = {**(request.metadata or {}), **filters["metadata"]}
153
+ elif value.get("metadata"):
154
+ request.metadata = {**(request.metadata or {}), **value["metadata"]}
155
+
156
+ return await service.update_assistant(assistant_id, request, user.identity)
157
+
158
+
159
+ @router.delete("/assistants/{assistant_id}")
160
+ async def delete_assistant(
161
+ assistant_id: str,
162
+ user: User = Depends(get_current_user),
163
+ service: AssistantService = Depends(get_assistant_service),
164
+ ):
165
+ """Delete assistant by ID"""
166
+ # Authorization check
167
+ ctx = build_auth_context(user, "assistants", "delete")
168
+ value = {"assistant_id": assistant_id}
169
+ await handle_event(ctx, value)
170
+
171
+ return await service.delete_assistant(assistant_id, user.identity)
172
+
173
+
174
+ @router.post(
175
+ "/assistants/{assistant_id}/latest",
176
+ response_model=Assistant,
177
+ response_model_by_alias=False,
178
+ )
179
+ async def set_assistant_latest(
180
+ assistant_id: str,
181
+ version: int = Body(..., embed=True, description="The version number to set as latest"),
182
+ user: User = Depends(get_current_user),
183
+ service: AssistantService = Depends(get_assistant_service),
184
+ ):
185
+ """Set the given version as the latest version of an assistant"""
186
+ return await service.set_assistant_latest(assistant_id, version, user.identity)
187
+
188
+
189
+ @router.post(
190
+ "/assistants/{assistant_id}/versions",
191
+ response_model=list[Assistant],
192
+ response_model_by_alias=False,
193
+ )
194
+ async def list_assistant_versions(
195
+ assistant_id: str,
196
+ user: User = Depends(get_current_user),
197
+ service: AssistantService = Depends(get_assistant_service),
198
+ ):
199
+ """List all versions of an assistant"""
200
+ return await service.list_assistant_versions(assistant_id, user.identity)
201
+
202
+
203
+ @router.get("/assistants/{assistant_id}/schemas")
204
+ async def get_assistant_schemas(
205
+ assistant_id: str,
206
+ user: User = Depends(get_current_user),
207
+ service: AssistantService = Depends(get_assistant_service),
208
+ ):
209
+ """Get input, output, state, config and context schemas for an assistant"""
210
+ return await service.get_assistant_schemas(assistant_id, user.identity)
211
+
212
+
213
+ @router.get("/assistants/{assistant_id}/graph")
214
+ async def get_assistant_graph(
215
+ assistant_id: str,
216
+ xray: bool | int | None = None,
217
+ user: User = Depends(get_current_user),
218
+ service: AssistantService = Depends(get_assistant_service),
219
+ ):
220
+ """Get the graph structure for visualization"""
221
+ # Default to False if not provided
222
+ xray_value = xray if xray is not None else False
223
+ return await service.get_assistant_graph(assistant_id, xray_value, user.identity)
224
+
225
+
226
+ @router.get("/assistants/{assistant_id}/subgraphs")
227
+ async def get_assistant_subgraphs(
228
+ assistant_id: str,
229
+ recurse: bool = False,
230
+ namespace: str | None = None,
231
+ user: User = Depends(get_current_user),
232
+ service: AssistantService = Depends(get_assistant_service),
233
+ ):
234
+ """Get subgraphs of an assistant"""
235
+ return await service.get_assistant_subgraphs(assistant_id, namespace, recurse, user.identity)