10xgraph-api 0.7.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 (203) hide show
  1. 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
  2. 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
  3. 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
  4. 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
  5. 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
  6. 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
  7. agentflow_cli/__init__.py +69 -0
  8. tenxgraph_api/__init__.py +23 -0
  9. tenxgraph_api/cli/__init__.py +6 -0
  10. tenxgraph_api/cli/capabilities.py +127 -0
  11. tenxgraph_api/cli/commands/__init__.py +49 -0
  12. tenxgraph_api/cli/commands/api.py +262 -0
  13. tenxgraph_api/cli/commands/audit.py +213 -0
  14. tenxgraph_api/cli/commands/build.py +305 -0
  15. tenxgraph_api/cli/commands/config.py +71 -0
  16. tenxgraph_api/cli/commands/demo.py +93 -0
  17. tenxgraph_api/cli/commands/eval.py +909 -0
  18. tenxgraph_api/cli/commands/init.py +531 -0
  19. tenxgraph_api/cli/commands/skills.py +532 -0
  20. tenxgraph_api/cli/commands/test.py +113 -0
  21. tenxgraph_api/cli/commands/version.py +47 -0
  22. tenxgraph_api/cli/config_editor/__init__.py +15 -0
  23. tenxgraph_api/cli/config_editor/schema.py +566 -0
  24. tenxgraph_api/cli/config_editor/server.py +240 -0
  25. tenxgraph_api/cli/config_editor/static/app.css +2 -0
  26. tenxgraph_api/cli/config_editor/static/app.js +3 -0
  27. tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
  28. tenxgraph_api/cli/config_editor/static/index.html +20 -0
  29. tenxgraph_api/cli/config_editor/store.py +120 -0
  30. tenxgraph_api/cli/config_editor/validation.py +377 -0
  31. tenxgraph_api/cli/constants.py +112 -0
  32. tenxgraph_api/cli/context.py +46 -0
  33. tenxgraph_api/cli/core/__init__.py +0 -0
  34. tenxgraph_api/cli/core/animations.py +550 -0
  35. tenxgraph_api/cli/core/config.py +279 -0
  36. tenxgraph_api/cli/core/output.py +625 -0
  37. tenxgraph_api/cli/core/prompts.py +220 -0
  38. tenxgraph_api/cli/core/screen.py +287 -0
  39. tenxgraph_api/cli/core/steps.py +621 -0
  40. tenxgraph_api/cli/core/theme.py +242 -0
  41. tenxgraph_api/cli/core/validation.py +251 -0
  42. tenxgraph_api/cli/exceptions.py +129 -0
  43. tenxgraph_api/cli/logger.py +84 -0
  44. tenxgraph_api/cli/main.py +936 -0
  45. tenxgraph_api/cli/templates/__init__.py +0 -0
  46. tenxgraph_api/cli/templates/defaults.py +844 -0
  47. tenxgraph_api/cli/templates/dev/.env.example +1 -0
  48. tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
  49. tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
  50. tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
  51. tenxgraph_api/cli/templates/prod/.env.example +75 -0
  52. tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
  53. tenxgraph_api/cli/templates/prod/.python-version +1 -0
  54. tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
  55. tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
  56. tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
  57. tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
  58. tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
  59. tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
  60. tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
  61. tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
  62. tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
  63. tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
  64. tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
  65. tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
  66. tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
  67. tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
  68. tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
  69. tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
  70. tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
  71. tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
  72. tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
  73. tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
  74. tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
  75. tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
  76. tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
  77. tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
  78. tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
  79. tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
  80. tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
  81. tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
  82. tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
  83. tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
  84. tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
  85. tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
  86. tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
  87. tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
  88. tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
  89. tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
  90. tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
  91. tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
  92. tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
  93. tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
  94. tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
  95. tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
  96. tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
  97. tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
  98. tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
  99. tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
  100. tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
  101. tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
  102. tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
  103. tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
  104. tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
  105. tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
  106. tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
  107. tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
  108. tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
  109. tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
  110. tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
  111. tenxgraph_api/py.typed +0 -0
  112. tenxgraph_api/src/__init__.py +0 -0
  113. tenxgraph_api/src/app/__init__.py +13 -0
  114. tenxgraph_api/src/app/core/__init__.py +15 -0
  115. tenxgraph_api/src/app/core/auth/__init__.py +0 -0
  116. tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
  117. tenxgraph_api/src/app/core/auth/authorization.py +379 -0
  118. tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
  119. tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
  120. tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
  121. tenxgraph_api/src/app/core/auth/permissions.py +354 -0
  122. tenxgraph_api/src/app/core/auth/request_config.py +130 -0
  123. tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
  124. tenxgraph_api/src/app/core/config/__init__.py +0 -0
  125. tenxgraph_api/src/app/core/config/graph_config.py +532 -0
  126. tenxgraph_api/src/app/core/config/media_settings.py +72 -0
  127. tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
  128. tenxgraph_api/src/app/core/config/settings.py +202 -0
  129. tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
  130. tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
  131. tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
  132. tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
  133. tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
  134. tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
  135. tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
  136. tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
  137. tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
  138. tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
  139. tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
  140. tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
  141. tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
  142. tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
  143. tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
  144. tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
  145. tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
  146. tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
  147. tenxgraph_api/src/app/core/utils/__init__.py +14 -0
  148. tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
  149. tenxgraph_api/src/app/loader.py +516 -0
  150. tenxgraph_api/src/app/main.py +180 -0
  151. tenxgraph_api/src/app/routers/__init__.py +4 -0
  152. tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
  153. tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
  154. tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
  155. tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
  156. tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
  157. tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
  158. tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
  159. tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
  160. tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
  161. tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
  162. tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
  163. tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
  164. tenxgraph_api/src/app/routers/evals/router.py +38 -0
  165. tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
  166. tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
  167. tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
  168. tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
  169. tenxgraph_api/src/app/routers/graph/router.py +821 -0
  170. tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
  171. tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
  172. tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
  173. tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
  174. tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
  175. tenxgraph_api/src/app/routers/media/__init__.py +506 -0
  176. tenxgraph_api/src/app/routers/media/router.py +242 -0
  177. tenxgraph_api/src/app/routers/media/schemas.py +39 -0
  178. tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
  179. tenxgraph_api/src/app/routers/ping/router.py +28 -0
  180. tenxgraph_api/src/app/routers/setup_router.py +60 -0
  181. tenxgraph_api/src/app/routers/store/__init__.py +4 -0
  182. tenxgraph_api/src/app/routers/store/router.py +200 -0
  183. tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
  184. tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
  185. tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
  186. tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
  187. tenxgraph_api/src/app/tasks/__init__.py +0 -0
  188. tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
  189. tenxgraph_api/src/app/utils/__init__.py +12 -0
  190. tenxgraph_api/src/app/utils/callable_helper.py +26 -0
  191. tenxgraph_api/src/app/utils/media/__init__.py +15 -0
  192. tenxgraph_api/src/app/utils/media/extractor.py +172 -0
  193. tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
  194. tenxgraph_api/src/app/utils/parse_output.py +17 -0
  195. tenxgraph_api/src/app/utils/response_helper.py +129 -0
  196. tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
  197. tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
  198. tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
  199. tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
  200. tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
  201. tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
  202. tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
  203. tenxgraph_api/src/app/worker.py +91 -0
@@ -0,0 +1,242 @@
1
+ """File upload, retrieval, and multimodal config endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import mimetypes
6
+ from typing import Any
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
9
+ from fastapi.logger import logger
10
+ from fastapi.responses import Response
11
+ from injectq.integrations import InjectAPI
12
+
13
+ from tenxgraph_api.src.app.core.auth.permissions import RequirePermission
14
+ from tenxgraph_api.src.app.core.config.media_settings import get_media_settings
15
+ from tenxgraph_api.src.app.routers.media import MediaService
16
+ from tenxgraph_api.src.app.routers.media.schemas import (
17
+ FileAccessUrlResponse,
18
+ FileInfoResponse,
19
+ FileUploadResponse,
20
+ MultimodalConfigResponse,
21
+ )
22
+ from tenxgraph_api.src.app.utils import success_response
23
+
24
+
25
+ router = APIRouter(tags=["Files"])
26
+
27
+ # Read uploads in 1 MiB chunks so a size-cap breach is caught before the whole body
28
+ # is buffered in memory.
29
+ _UPLOAD_CHUNK_BYTES = 1024 * 1024
30
+
31
+ # Stored types come from the uploader. Only types a browser displays without running
32
+ # script are served inline; the rest are downloads.
33
+ _INLINE_TYPES = frozenset(
34
+ {
35
+ "image/png",
36
+ "image/jpeg",
37
+ "image/gif",
38
+ "image/webp",
39
+ "image/avif",
40
+ "image/bmp",
41
+ "application/pdf",
42
+ "text/plain",
43
+ }
44
+ )
45
+ _INLINE_PREFIXES = ("audio/", "video/")
46
+
47
+
48
+ def _is_active_type(mime: str) -> bool:
49
+ """Whether a browser would run this type as a page or script, even from a blob: URL.
50
+
51
+ Such files are served as opaque bytes, so neither a navigation nor ``response.blob()``
52
+ keeps the active type.
53
+ """
54
+ subtype = mime.partition("/")[2]
55
+ return (
56
+ subtype in {"html", "xml", "xhtml+xml", "svg+xml"}
57
+ or subtype.endswith("+xml")
58
+ or "javascript" in subtype
59
+ or "ecmascript" in subtype
60
+ )
61
+
62
+
63
+ def _download_response(file_id: str, data: bytes, mime_type: str | None) -> Response:
64
+ """Serve stored bytes so an uploaded HTML or SVG file cannot run script on this origin."""
65
+ mime = (mime_type or "").split(";", 1)[0].strip().lower()
66
+ if mime in _INLINE_TYPES or mime.startswith(_INLINE_PREFIXES):
67
+ disposition = "inline"
68
+ else:
69
+ disposition = f'attachment; filename="{file_id}"'
70
+ if not mime or _is_active_type(mime):
71
+ mime = "application/octet-stream"
72
+ headers = {"Content-Disposition": disposition, "X-Content-Type-Options": "nosniff"}
73
+ if mime != "application/pdf": # browsers will not render a PDF under a sandbox CSP
74
+ headers["Content-Security-Policy"] = "default-src 'none'; sandbox"
75
+ return Response(content=data, media_type=mime or "application/octet-stream", headers=headers)
76
+
77
+
78
+ # ------------------------------------------------------------------
79
+ # 4.1 POST /v1/files/upload
80
+ # ------------------------------------------------------------------
81
+ @router.post(
82
+ "/v1/files/upload",
83
+ summary="Upload a file (image, audio, document)",
84
+ response_model=FileUploadResponse,
85
+ description=(
86
+ "Accepts a multipart file upload. Stores binary in the configured "
87
+ "MediaStore and optionally extracts text for documents."
88
+ ),
89
+ )
90
+ async def upload_file(
91
+ request: Request,
92
+ file: UploadFile,
93
+ service: MediaService = InjectAPI(MediaService),
94
+ user: dict[str, Any] = Depends(RequirePermission("files", "upload")),
95
+ ):
96
+ if file.filename is None:
97
+ raise HTTPException(status_code=400, detail="filename is required")
98
+
99
+ media_settings = get_media_settings()
100
+ mime = file.content_type or mimetypes.guess_type(file.filename)[0] or "application/octet-stream"
101
+
102
+ if not media_settings.is_content_type_allowed(mime):
103
+ raise HTTPException(status_code=415, detail=f"Content type not allowed: {mime}")
104
+
105
+ # Read with a running size cap instead of ``await file.read()``. A chunked upload
106
+ # (no Content-Length) slips past RequestSizeLimitMiddleware, so a naive full read
107
+ # would let a multi-GB body OOM-kill the worker. Stop as soon as we exceed the
108
+ # limit and reject, never materializing more than max_size + one chunk.
109
+ max_bytes = int(media_settings.MEDIA_MAX_SIZE_MB * 1024 * 1024)
110
+ chunks: list[bytes] = []
111
+ total = 0
112
+ while True:
113
+ chunk = await file.read(_UPLOAD_CHUNK_BYTES)
114
+ if not chunk:
115
+ break
116
+ total += len(chunk)
117
+ if total > max_bytes:
118
+ raise HTTPException(
119
+ status_code=413,
120
+ detail=f"File exceeds maximum {media_settings.MEDIA_MAX_SIZE_MB}MB",
121
+ )
122
+ chunks.append(chunk)
123
+ data = b"".join(chunks)
124
+ if not data:
125
+ raise HTTPException(status_code=400, detail="Empty file")
126
+
127
+ logger.info("File upload: %s (%s, %d bytes)", file.filename, mime, len(data))
128
+
129
+ try:
130
+ # Record the uploader as the owner, so the file cannot later be read by a
131
+ # different user who knows the id.
132
+ result = await service.upload_file(data, file.filename, mime, user_id=user.get("user_id"))
133
+ except ValueError as exc:
134
+ raise HTTPException(status_code=413, detail=str(exc))
135
+
136
+ return success_response(FileUploadResponse(**result), request)
137
+
138
+
139
+ # ------------------------------------------------------------------
140
+ # 4.2 GET /v1/files/{file_id} — raw binary download
141
+ # ------------------------------------------------------------------
142
+ @router.get(
143
+ "/v1/files/{file_id}",
144
+ summary="Retrieve file binary",
145
+ description="Returns the raw binary with the correct Content-Type.",
146
+ )
147
+ async def get_file(
148
+ file_id: str,
149
+ service: MediaService = InjectAPI(MediaService),
150
+ user: dict[str, Any] = Depends(RequirePermission("files", "read")),
151
+ ):
152
+ try:
153
+ # Authentication alone is not enough: without this, ANY authenticated user
154
+ # who knew a file_id could download another user's file.
155
+ await service.ensure_can_access(file_id, user.get("user_id"))
156
+ data, mime_type = await service.get_file(file_id, user.get("user_id"))
157
+ except KeyError:
158
+ raise HTTPException(status_code=404, detail="File not found")
159
+ except PermissionError:
160
+ # 404, not 403: do not confirm that someone else's file_id exists.
161
+ raise HTTPException(status_code=404, detail="File not found")
162
+
163
+ return _download_response(file_id, data, mime_type)
164
+
165
+
166
+ # ------------------------------------------------------------------
167
+ # 4.2 GET /v1/files/{file_id}/info — metadata only
168
+ # ------------------------------------------------------------------
169
+ @router.get(
170
+ "/v1/files/{file_id}/info",
171
+ summary="Retrieve file metadata",
172
+ response_model=FileInfoResponse,
173
+ )
174
+ async def get_file_info(
175
+ request: Request,
176
+ file_id: str,
177
+ service: MediaService = InjectAPI(MediaService),
178
+ user: dict[str, Any] = Depends(RequirePermission("files", "read")),
179
+ ):
180
+ try:
181
+ await service.ensure_can_access(file_id, user.get("user_id"))
182
+ info = await service.get_file_info(file_id, user.get("user_id"))
183
+ except KeyError:
184
+ raise HTTPException(status_code=404, detail="File not found")
185
+ except PermissionError:
186
+ raise HTTPException(status_code=404, detail="File not found")
187
+
188
+ return success_response(FileInfoResponse(**info), request)
189
+
190
+
191
+ @router.get(
192
+ "/v1/files/{file_id}/url",
193
+ summary="Retrieve direct access URL",
194
+ response_model=FileAccessUrlResponse,
195
+ )
196
+ async def get_file_access_url(
197
+ request: Request,
198
+ file_id: str,
199
+ service: MediaService = InjectAPI(MediaService),
200
+ user: dict[str, Any] = Depends(RequirePermission("files", "read")),
201
+ ):
202
+ try:
203
+ # Especially important here: this hands out a *signed direct URL*, which
204
+ # bypasses the API entirely. Handing one to a non-owner would leak the file
205
+ # even after this check was added elsewhere.
206
+ await service.ensure_can_access(file_id, user.get("user_id"))
207
+ info = await service.get_file_info(file_id)
208
+ except KeyError:
209
+ raise HTTPException(status_code=404, detail="File not found")
210
+ except PermissionError:
211
+ raise HTTPException(status_code=404, detail="File not found")
212
+
213
+ payload = FileAccessUrlResponse(
214
+ file_id=file_id,
215
+ url=info["direct_url"] or f"/v1/files/{file_id}",
216
+ expires_at=info.get("direct_url_expires_at"),
217
+ mime_type=info["mime_type"],
218
+ )
219
+ return success_response(payload, request)
220
+
221
+
222
+ # ------------------------------------------------------------------
223
+ # 4.4 GET /v1/config/multimodal — read current multimodal config
224
+ # ------------------------------------------------------------------
225
+ @router.get(
226
+ "/v1/config/multimodal",
227
+ summary="Get multimodal configuration",
228
+ response_model=MultimodalConfigResponse,
229
+ )
230
+ async def get_multimodal_config(
231
+ request: Request,
232
+ user: dict[str, Any] = Depends(RequirePermission("config", "read")),
233
+ ):
234
+ settings = get_media_settings()
235
+ return success_response(
236
+ MultimodalConfigResponse(
237
+ media_storage_type=settings.MEDIA_STORAGE_TYPE.value,
238
+ media_max_size_mb=settings.MEDIA_MAX_SIZE_MB,
239
+ document_handling=settings.DOCUMENT_HANDLING,
240
+ ),
241
+ request,
242
+ )
@@ -0,0 +1,39 @@
1
+ """Schemas for the media / file upload endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class FileUploadResponse(BaseModel):
9
+ file_id: str = Field(..., description="Opaque storage key")
10
+ mime_type: str
11
+ size_bytes: int
12
+ filename: str
13
+ extracted_text: str | None = None
14
+ url: str = Field(..., description="Relative retrieval URL")
15
+ direct_url: str | None = None
16
+ direct_url_expires_at: int | None = None
17
+
18
+
19
+ class FileInfoResponse(BaseModel):
20
+ file_id: str
21
+ mime_type: str
22
+ size_bytes: int
23
+ filename: str | None = None
24
+ extracted_text: str | None = None
25
+ direct_url: str | None = None
26
+ direct_url_expires_at: int | None = None
27
+
28
+
29
+ class FileAccessUrlResponse(BaseModel):
30
+ file_id: str
31
+ url: str
32
+ expires_at: int | None = None
33
+ mime_type: str
34
+
35
+
36
+ class MultimodalConfigResponse(BaseModel):
37
+ media_storage_type: str
38
+ media_max_size_mb: float
39
+ document_handling: str
@@ -0,0 +1,4 @@
1
+ from .router import router
2
+
3
+
4
+ __all__ = ["router"]
@@ -0,0 +1,28 @@
1
+ from fastapi import APIRouter, Request
2
+
3
+ from tenxgraph_api.src.app.utils.response_helper import success_response
4
+ from tenxgraph_api.src.app.utils.swagger_helper import generate_swagger_responses
5
+
6
+
7
+ router = APIRouter(
8
+ tags=["Ping"],
9
+ )
10
+
11
+
12
+ @router.get(
13
+ "/ping",
14
+ summary="Ping the server",
15
+ responses=generate_swagger_responses(str), # type: ignore
16
+ description="Check the server's health",
17
+ openapi_extra={},
18
+ )
19
+ async def ping_server(
20
+ request: Request,
21
+ ):
22
+ """
23
+ Invoke the graph with the provided input and return the final result.
24
+ """
25
+ return success_response(
26
+ "pong",
27
+ request,
28
+ )
@@ -0,0 +1,60 @@
1
+ from fastapi import APIRouter, FastAPI
2
+
3
+ from .checkpointer.router import router as checkpointer_router
4
+ from .evals import router as evals_router
5
+ from .graph import router as graph_router
6
+ from .media.router import router as media_router
7
+ from .ping.router import router as ping_router
8
+ from .store import router as store_router
9
+
10
+
11
+ AG_UI_INSTALL_HINT = 'pip install "10xgraph-api[ag-ui]"'
12
+
13
+
14
+ def _import_ag_ui_router() -> APIRouter:
15
+ from .ag_ui import router
16
+
17
+ return router
18
+
19
+
20
+ def _ag_ui_router() -> APIRouter:
21
+ """Import the AG-UI router, which needs the optional ``ag-ui-protocol`` package.
22
+
23
+ Imported only when ``ag_ui.enabled`` is set, so servers that leave it off never need the
24
+ package installed.
25
+ """
26
+ try:
27
+ return _import_ag_ui_router()
28
+ except ModuleNotFoundError as exc:
29
+ if exc.name and exc.name.split(".")[0] == "ag_ui":
30
+ raise RuntimeError(
31
+ "ag_ui is enabled in 10xgraph.json but the AG-UI SDK is not installed. "
32
+ f"Install it with: {AG_UI_INSTALL_HINT}"
33
+ ) from exc
34
+ raise
35
+
36
+
37
+ def init_routes(app: FastAPI, *, evals: bool = True, ag_ui: bool = False):
38
+ """
39
+ Initialize the routes for the FastAPI application.
40
+
41
+ This function includes the graph and checkpointer routers for 10xGraph functionality.
42
+ Auth and GraphQL routers are disabled for now.
43
+
44
+ Args:
45
+ app (FastAPI): The FastAPI application instance to which the routes
46
+ will be added.
47
+ evals (bool): Mount the evals report viewer. It has no auth, so production
48
+ leaves it out.
49
+ ag_ui (bool): Mount the AG-UI endpoint (``POST /v1/ag-ui``). Off unless
50
+ ``ag_ui.enabled`` is set in 10xgraph.json.
51
+ """
52
+ app.include_router(graph_router)
53
+ app.include_router(checkpointer_router)
54
+ app.include_router(store_router)
55
+ if evals:
56
+ app.include_router(evals_router)
57
+ if ag_ui:
58
+ app.include_router(_ag_ui_router())
59
+ app.include_router(ping_router)
60
+ app.include_router(media_router)
@@ -0,0 +1,4 @@
1
+ from .router import router
2
+
3
+
4
+ __all__ = ["router"]
@@ -0,0 +1,200 @@
1
+ """Store router module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter, Body, Depends, HTTPException, Request, status
8
+ from injectq.integrations import InjectAPI
9
+
10
+ from tenxgraph_api.src.app.core.auth.permissions import RequirePermission
11
+ from tenxgraph_api.src.app.utils.response_helper import success_response
12
+ from tenxgraph_api.src.app.utils.swagger_helper import generate_swagger_responses
13
+
14
+ from .schemas.store_schemas import (
15
+ DeleteMemorySchema,
16
+ ForgetMemorySchema,
17
+ GetMemorySchema,
18
+ ListMemoriesSchema,
19
+ MemoryCreateResponseSchema,
20
+ MemoryItemResponseSchema,
21
+ MemoryListResponseSchema,
22
+ MemoryOperationResponseSchema,
23
+ MemorySearchResponseSchema,
24
+ SearchMemorySchema,
25
+ StoreMemorySchema,
26
+ UpdateMemorySchema,
27
+ )
28
+ from .services.store_service import StoreService
29
+
30
+
31
+ router = APIRouter(tags=["store"])
32
+
33
+
34
+ @router.post(
35
+ "/v1/store/memories",
36
+ status_code=status.HTTP_200_OK,
37
+ responses=generate_swagger_responses(MemoryCreateResponseSchema),
38
+ summary="Store a memory",
39
+ description="Persist a memory payload using the configured store backend.",
40
+ )
41
+ async def create_memory(
42
+ request: Request,
43
+ payload: StoreMemorySchema,
44
+ service: StoreService = InjectAPI(StoreService),
45
+ user: dict[str, Any] = Depends(RequirePermission("store", "write")),
46
+ ):
47
+ """Store a memory item using the configured store."""
48
+
49
+ result = await service.store_memory(payload, user)
50
+ return success_response(result, request, message="Memory stored successfully")
51
+
52
+
53
+ @router.post(
54
+ "/v1/store/search",
55
+ status_code=status.HTTP_200_OK,
56
+ responses=generate_swagger_responses(MemorySearchResponseSchema),
57
+ summary="Search memories",
58
+ description="Search memories stored in the backend based on semantic similarity and filters.",
59
+ )
60
+ async def search_memories(
61
+ request: Request,
62
+ payload: SearchMemorySchema,
63
+ service: StoreService = InjectAPI(StoreService),
64
+ user: dict[str, Any] = Depends(RequirePermission("store", "read")),
65
+ ):
66
+ """Search stored memories."""
67
+ if not payload.query or (isinstance(payload.query, str) and not payload.query.strip()):
68
+ raise HTTPException(status_code=422, detail="query is required and cannot be empty")
69
+
70
+ result = await service.search_memories(payload, user)
71
+ return success_response(result, request)
72
+
73
+
74
+ # NOTE: static sub-paths (/list, /forget) MUST be registered before the
75
+ # /{memory_id} catch-all below — FastAPI matches top-down, so otherwise
76
+ # `POST /v1/store/memories/list` binds to `{memory_id="list"}` and never lists.
77
+ @router.post(
78
+ "/v1/store/memories/list",
79
+ status_code=status.HTTP_200_OK,
80
+ responses=generate_swagger_responses(MemoryListResponseSchema),
81
+ summary="List memories",
82
+ description="List memories from the configured store backend.",
83
+ )
84
+ async def list_memories(
85
+ request: Request,
86
+ payload: ListMemoriesSchema | None = Body(
87
+ default=None,
88
+ description="Optional configuration, limit, and options for listing memories.",
89
+ ),
90
+ service: StoreService = InjectAPI(StoreService),
91
+ user: dict[str, Any] = Depends(RequirePermission("store", "read")),
92
+ ):
93
+ """List stored memories."""
94
+
95
+ if payload and payload.limit is not None and payload.limit <= 0:
96
+ raise HTTPException(status_code=422, detail="limit must be > 0")
97
+
98
+ if payload is None:
99
+ payload = ListMemoriesSchema()
100
+ cfg = payload.config or {}
101
+ opts = payload.options
102
+ result = await service.list_memories(cfg, user, limit=payload.limit, options=opts)
103
+ return success_response(result, request)
104
+
105
+
106
+ @router.put(
107
+ "/v1/store/memories/{memory_id}",
108
+ status_code=status.HTTP_200_OK,
109
+ responses=generate_swagger_responses(MemoryOperationResponseSchema),
110
+ summary="Update a memory",
111
+ description="Update the content or metadata of a stored memory.",
112
+ )
113
+ async def update_memory(
114
+ request: Request,
115
+ memory_id: str,
116
+ payload: UpdateMemorySchema,
117
+ service: StoreService = InjectAPI(StoreService),
118
+ user: dict[str, Any] = Depends(RequirePermission("store", "write")),
119
+ ):
120
+ """Update a stored memory."""
121
+ if not memory_id or not memory_id.strip():
122
+ raise HTTPException(status_code=422, detail="memory_id is required and cannot be empty")
123
+
124
+ result = await service.update_memory(memory_id, payload, user)
125
+ return success_response(result, request, message="Memory updated successfully")
126
+
127
+
128
+ @router.delete(
129
+ "/v1/store/memories/{memory_id}",
130
+ status_code=status.HTTP_200_OK,
131
+ responses=generate_swagger_responses(MemoryOperationResponseSchema),
132
+ summary="Delete a memory",
133
+ description="Delete a stored memory by its identifier.",
134
+ )
135
+ async def delete_memory(
136
+ request: Request,
137
+ memory_id: str,
138
+ payload: DeleteMemorySchema | None = Body(
139
+ default=None,
140
+ description="Optional configuration overrides forwarded to the store backend.",
141
+ ),
142
+ service: StoreService = InjectAPI(StoreService),
143
+ user: dict[str, Any] = Depends(RequirePermission("store", "delete")),
144
+ ):
145
+ """Delete a stored memory."""
146
+ if not memory_id or not memory_id.strip():
147
+ raise HTTPException(status_code=422, detail="memory_id is required and cannot be empty")
148
+
149
+ config_payload = payload.config if payload else {}
150
+ options_payload = payload.options if payload else None
151
+ result = await service.delete_memory(memory_id, config_payload, user, options=options_payload)
152
+ return success_response(result, request, message="Memory deleted successfully")
153
+
154
+
155
+ @router.post(
156
+ "/v1/store/memories/forget",
157
+ status_code=status.HTTP_200_OK,
158
+ responses=generate_swagger_responses(MemoryOperationResponseSchema),
159
+ summary="Forget memories",
160
+ description="Forget memories matching the provided filters from the store backend.",
161
+ )
162
+ async def forget_memory(
163
+ request: Request,
164
+ payload: ForgetMemorySchema,
165
+ service: StoreService = InjectAPI(StoreService),
166
+ user: dict[str, Any] = Depends(RequirePermission("store", "delete")),
167
+ ):
168
+ """Forget memories based on filters."""
169
+
170
+ result = await service.forget_memory(payload, user)
171
+ return success_response(result, request, message="Memories removed successfully")
172
+
173
+
174
+ # Registered last so the static /list and /forget paths above take precedence
175
+ # over this {memory_id} catch-all.
176
+ @router.post(
177
+ "/v1/store/memories/{memory_id}",
178
+ status_code=status.HTTP_200_OK,
179
+ responses=generate_swagger_responses(MemoryItemResponseSchema),
180
+ summary="Get a memory",
181
+ description="Retrieve a memory by its identifier from the configured store backend.",
182
+ )
183
+ async def get_memory(
184
+ request: Request,
185
+ memory_id: str,
186
+ payload: GetMemorySchema | None = Body(
187
+ default=None,
188
+ description="Optional configuration and options for retrieving the memory.",
189
+ ),
190
+ service: StoreService = InjectAPI(StoreService),
191
+ user: dict[str, Any] = Depends(RequirePermission("store", "read")),
192
+ ):
193
+ """Get a memory by ID."""
194
+ if not memory_id or not memory_id.strip():
195
+ raise HTTPException(status_code=422, detail="memory_id is required and cannot be empty")
196
+
197
+ cfg = payload.config if payload else {}
198
+ opts = payload.options if payload else None
199
+ result = await service.get_memory(memory_id, cfg, user, options=opts)
200
+ return success_response(result, request)
@@ -0,0 +1,28 @@
1
+ from .store_schemas import (
2
+ BaseConfigSchema,
3
+ DeleteMemorySchema,
4
+ ForgetMemorySchema,
5
+ MemoryCreateResponseSchema,
6
+ MemoryItemResponseSchema,
7
+ MemoryListResponseSchema,
8
+ MemoryOperationResponseSchema,
9
+ MemorySearchResponseSchema,
10
+ SearchMemorySchema,
11
+ StoreMemorySchema,
12
+ UpdateMemorySchema,
13
+ )
14
+
15
+
16
+ __all__ = [
17
+ "BaseConfigSchema",
18
+ "DeleteMemorySchema",
19
+ "ForgetMemorySchema",
20
+ "MemoryCreateResponseSchema",
21
+ "MemoryItemResponseSchema",
22
+ "MemoryListResponseSchema",
23
+ "MemoryOperationResponseSchema",
24
+ "MemorySearchResponseSchema",
25
+ "SearchMemorySchema",
26
+ "StoreMemorySchema",
27
+ "UpdateMemorySchema",
28
+ ]