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,108 @@
1
+ ---
2
+ applyTo: "**"
3
+ ---
4
+
5
+ # 10xGraph project instructions
6
+
7
+ This repo uses **10xGraph** — a multi-agent framework that wraps the official OpenAI and Google SDKs behind a unified graph, agent, tool, state, storage, API, CLI, and TypeScript-client interface.
8
+
9
+ When generating, refactoring, or debugging code in this repo, prefer 10xGraph's own abstractions over hand-rolled equivalents.
10
+
11
+ ## Public package names (use these in user-facing examples)
12
+
13
+ - Python core SDK: `10xgraph` — `pip install 10xgraph` — source under `tenxgraph/` in https://github.com/10xGraph/10xGraph
14
+ - Python API/CLI SDK: `10xgraph-api` — `pip install 10xgraph-api` — source under `tenxgraph_api/` in https://github.com/10xGraph/10xgraph-api
15
+ - TypeScript SDK: `@10xscale/agentflow-client` — `npm install @10xscale/agentflow-client` — source under `agentflow-client/src`
16
+ - Docs: https://10xgraph.com/ (source of truth for public API names)
17
+ - Playground: `10xgraph play` (after the CLI is installed)
18
+
19
+ Never use repository folder names (e.g. `10xgraph-api`) in install commands or user-facing docs — use the published package names above.
20
+
21
+ ## Architecture overview
22
+
23
+ Three published packages, one request flow:
24
+
25
+ 1. `@10xscale/agentflow-client` or another HTTP caller sends messages with `config.thread_id`.
26
+ 2. FastAPI (`10xgraph-api`) receives the request through auth and routers.
27
+ 3. `GraphService` invokes or streams against the compiled graph loaded at startup.
28
+ 4. The compiled graph loads state through the checkpointer when a `thread_id` exists.
29
+ 5. Graph nodes run and update `AgentState`.
30
+ 6. Checkpointer saves state, messages, and thread metadata.
31
+ 7. API returns JSON or SSE chunks to the caller.
32
+
33
+ Compile graphs once at startup. Keep graph code storage-agnostic; wire dependencies through compile arguments, `InjectQ`, or `10xgraph.json`.
34
+
35
+ ## Core abstractions to reach for
36
+
37
+ - Build workflows with `StateGraph`, `Agent`, `ToolNode`, `AgentState`, and `Message`.
38
+ - **Use prebuilt agents for common patterns before hand-writing graph loops:**
39
+ - `ReactAgent` — standard tool-calling loop (use by default)
40
+ - `PlanActReflectAgent` — plan → act → reflect loop for complex multi-step tasks
41
+ - `StructuredOutputAgent` — forces JSON output matching a Pydantic schema
42
+ - `SupervisorTeamAgent` — central supervisor routes to specialist worker agents
43
+ - `SwarmAgent` — peer-to-peer agent handoff without a central supervisor
44
+ - `RAGAgent` — retrieves documents from a vector store before each LLM call
45
+ - `AudioAgent` — realtime full-duplex voice agent (Gemini Live); needs the `[realtime]` extra and is driven by `arealtime()`, not `invoke`/`stream`
46
+ - Persist conversation state with **checkpointers**. Use **stores** only for cross-thread memory.
47
+ - Inject business services through **`InjectQ`**, not module-level globals.
48
+ - Keep API/CLI graph modules storage-agnostic; wire dependencies via `10xgraph.json`.
49
+ - Every persisted interaction must include `config.thread_id`.
50
+ - Tools need docstrings and type annotations so model-facing schemas are useful.
51
+ - Injectable parameters (`state`, `config`, `tool_call_id`) are hidden from the model schema.
52
+ - For production, avoid process-local storage for shared state — use durable checkpointer/store backends.
53
+ - Add observability, audit, or business-logic side effects by registering a `GraphLifecycleHook` on `CallbackManager` — do not wrap `ainvoke()` / `astream()` calls in application code to achieve the same result.
54
+
55
+ ## Agent configuration quick reference
56
+
57
+ ```python
58
+ Agent(
59
+ model="gpt-4o", # required; provider auto-detected
60
+ provider="openai", # explicit or omit for auto-detect
61
+ base_url="http://localhost:11434/v1", # for Ollama / DeepSeek / OpenRouter
62
+ output_type="text", # "text" | "image" | "video" | "audio"
63
+ output_schema=MyPydanticModel, # force structured JSON output
64
+ system_prompt=[{"role": "system", "content": "Help {user_name}."}], # state interpolation
65
+ tool_node=tool_node, # ToolNode instance or graph node name string
66
+ tools_tags={"search"}, # expose only matching-tagged tools
67
+ extra_messages=[...], # injected after system_prompt every call
68
+ trim_context=True, # trim old messages to fit token limits
69
+ reasoning_config={"effort": "medium"}, # on by default; None to disable
70
+ retry_config=True, # default 3 retries with backoff
71
+ fallback_models=["gpt-4o-mini"], # tried in order after retries exhausted
72
+ memory=MemoryConfig(...), # long-term memory store wiring
73
+ skills=SkillConfig(...), # skills discovery and exposure
74
+ api_style="chat", # "chat" | "responses" (OpenAI only)
75
+ )
76
+ ```
77
+
78
+ ## Key configuration (`10xgraph.json`)
79
+
80
+ ```json
81
+ {
82
+ "agent": "graph.agent:app",
83
+ "env": ".env",
84
+ "auth": null,
85
+ "checkpointer": null,
86
+ "injectq": null,
87
+ "store": null,
88
+ "redis": null,
89
+ "thread_name_generator": null
90
+ }
91
+ ```
92
+
93
+ Set `"auth": {"method": "jwt"}` and `JWT_SECRET_KEY` in `.env` to enable JWT authentication.
94
+
95
+ ## Important conventions
96
+
97
+ - A compiled graph is loaded once at API startup and reused per request.
98
+ - `thread_id` is the continuity key for conversation state — always pass it for persisted interactions.
99
+ - Auth and request permissions belong in API middleware/routers, not inside graph nodes.
100
+ - Long-term memory store records are cross-thread knowledge, not thread history.
101
+ - Rate limiting config lives in `10xgraph.json` under `"rate_limit"`: backend options are `"memory"` or `"redis"`.
102
+ - `reasoning_config` is on by default — disable explicitly with `reasoning_config=None` when not needed.
103
+
104
+ ## Verifying behavior
105
+
106
+ Public names and behavior should match `agentflow-docs/docs` or https://10xgraph.com/. Implementation in the `10xgraph` and `10xgraph-api` repos and `agentflow-client/src/` shows *how* — only consult source after docs establish *what*.
107
+
108
+ That file points Copilot at the installed skill bundle under `.github/skills/10xgraph/`.
tenxgraph_api/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,13 @@
1
+ # Copyright 2021 Shudipto Trafder
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
@@ -0,0 +1,15 @@
1
+ from .config.settings import LOGGER_NAME, Settings, get_settings, logger
2
+ from .config.setup_logs import init_logger
3
+ from .config.setup_middleware import setup_middleware
4
+ from .exceptions.handle_errors import init_errors_handler
5
+
6
+
7
+ __all__ = [
8
+ "LOGGER_NAME",
9
+ "Settings",
10
+ "get_settings",
11
+ "init_errors_handler",
12
+ "init_logger",
13
+ "logger",
14
+ "setup_middleware",
15
+ ]
File without changes
@@ -0,0 +1,38 @@
1
+ from typing import Any
2
+
3
+ from fastapi import Depends, Request, Response
4
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
5
+ from injectq.integrations import InjectAPI
6
+
7
+ from tenxgraph_api.src.app.core import logger
8
+ from tenxgraph_api.src.app.core.auth.base_auth import BaseAuth
9
+ from tenxgraph_api.src.app.core.config.graph_config import GraphConfig
10
+
11
+
12
+ def verify_current_user(
13
+ request: Request,
14
+ response: Response,
15
+ credential: HTTPAuthorizationCredentials = Depends(
16
+ HTTPBearer(auto_error=False),
17
+ ),
18
+ config: GraphConfig = InjectAPI(GraphConfig),
19
+ auth_backend: BaseAuth = InjectAPI(BaseAuth),
20
+ ) -> dict[str, Any]:
21
+ # check auth backend
22
+ user = {}
23
+ backend = config.auth_config()
24
+ if not backend:
25
+ return user
26
+
27
+ if not auth_backend:
28
+ logger.error("Auth backend is not configured")
29
+ return user
30
+
31
+ user: dict | None = auth_backend.authenticate(
32
+ request,
33
+ response,
34
+ credential,
35
+ )
36
+ if user and "user_id" not in user:
37
+ logger.error("Authentication failed: 'user_id' not found in user info")
38
+ return user or {}
@@ -0,0 +1,379 @@
1
+ """
2
+ Authorization backend system for 10xGraph CLI.
3
+
4
+ This module provides an authorization interface that developers can implement
5
+ to add resource-level access control to their 10xGraph applications.
6
+ """
7
+
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from typing import Any
11
+
12
+
13
+ logger = logging.getLogger("tenxgraph_api.authorization")
14
+
15
+ # The /v1/store routes require these. Core releases up to 0.9.1 named them "memory:*", so a
16
+ # "*" role would never reach the memory routes; they are added here until core is bumped.
17
+ STORE_SCOPES = frozenset({"store:read", "store:write", "store:delete"})
18
+ LEGACY_MEMORY_SCOPES = frozenset({"memory:read", "memory:write", "memory:delete"})
19
+
20
+
21
+ def all_scopes() -> frozenset[str]:
22
+ """Every scope the API can require, which is what a ``"*"`` role is granted."""
23
+ from tenxgraph.core.authz import ALL_SCOPES
24
+
25
+ return (frozenset(ALL_SCOPES) - LEGACY_MEMORY_SCOPES) | STORE_SCOPES
26
+
27
+
28
+ class AuthorizationBackend(ABC):
29
+ """
30
+ Abstract base class for authorization backends.
31
+
32
+ Developers should implement this class to define custom authorization logic
33
+ for their 10xGraph applications. The authorize method is called before
34
+ any resource operation to check if the user has permission.
35
+
36
+ Example:
37
+ class MyAuthorizationBackend(AuthorizationBackend):
38
+ async def authorize(self, user, resource, action, resource_id=None, **context):
39
+ # Check if user has permission
40
+ if user.get("role") == "admin":
41
+ return True
42
+ # Add custom logic here
43
+ return False
44
+ """
45
+
46
+ @abstractmethod
47
+ async def authorize(
48
+ self,
49
+ user: dict[str, Any],
50
+ resource: str,
51
+ action: str,
52
+ resource_id: str | None = None,
53
+ **context: Any,
54
+ ) -> bool:
55
+ """
56
+ Check if user can perform action on resource.
57
+
58
+ Args:
59
+ user: User information dictionary containing at least 'user_id'
60
+ resource: Resource type (e.g., 'graph', 'checkpointer', 'store')
61
+ action: Action to perform (e.g., 'invoke', 'stream', 'read', 'write', 'delete')
62
+ resource_id: Optional specific resource identifier (e.g., thread_id, namespace)
63
+ **context: Additional context for authorization decision
64
+
65
+ Returns:
66
+ bool: True if authorized, False otherwise
67
+
68
+ Raises:
69
+ Exception: Can raise exceptions for auth failures or errors
70
+ """
71
+
72
+ def isolation_scope(self) -> str:
73
+ """The data-isolation policy this backend implies, sent to storage per request.
74
+
75
+ The API stamps this (server-side, non-hijackable) into ``config["policy"]`` so the
76
+ checkpointer scopes storage the *same* way this backend decides access -- no
77
+ contradiction between "allow_all" authz and a data layer that still filters.
78
+
79
+ - ``"owner"`` -> storage is scoped to the caller's ``user_id`` (owner-only).
80
+ - ``"none"`` -> storage is not scoped (any authenticated user sees everything).
81
+
82
+ Defaults to ``"none"``. Override (or return ``"owner"``) to make a custom backend
83
+ drive per-user storage isolation too.
84
+ """
85
+ return "none"
86
+
87
+ def scopes_for(self, user: dict[str, Any]) -> list[str] | None:
88
+ """Return the scopes this identity is granted, or None for "unrestricted".
89
+
90
+ ``RequirePermission`` checks the required ``"<resource>:<action>"`` scope against
91
+ this list. Returning ``None`` means the identity is not scope-restricted
92
+ (permissive -- the default, so nothing breaks until scopes are issued).
93
+
94
+ The default passes through scopes the identity already carries
95
+ (``user["scopes"]``, e.g. from a JWT ``scope`` claim). A role-based backend
96
+ overrides this to map ``user["roles"]`` to scopes.
97
+ """
98
+ scopes = user.get("scopes") if isinstance(user, dict) else None
99
+ if isinstance(scopes, list | tuple | set | frozenset):
100
+ return list(scopes)
101
+ return None
102
+
103
+
104
+ class DefaultAuthorizationBackend(AuthorizationBackend):
105
+ """
106
+ Default authorization backend that allows all authenticated users.
107
+
108
+ This implementation performs basic authentication check (user has user_id)
109
+ but allows all operations. Use this as a starting point or for development.
110
+
111
+ For production use, implement a custom AuthorizationBackend with proper
112
+ access control logic based on your application's requirements.
113
+ """
114
+
115
+ async def authorize(
116
+ self,
117
+ user: dict[str, Any],
118
+ resource: str,
119
+ action: str,
120
+ resource_id: str | None = None,
121
+ **context: Any,
122
+ ) -> bool:
123
+ """
124
+ Allow all authenticated users to perform any action.
125
+
126
+ Args:
127
+ user: User information dictionary
128
+ resource: Resource type (not used in default implementation)
129
+ action: Action to perform (not used in default implementation)
130
+ resource_id: Optional resource identifier (not used in default implementation)
131
+ **context: Additional context (not used in default implementation)
132
+
133
+ Returns:
134
+ bool: True if user has 'user_id', False otherwise
135
+ """
136
+ # Only check if user is authenticated (has user_id)
137
+ return bool(user.get("user_id"))
138
+
139
+
140
+ class OwnershipAuthorizationBackend(AuthorizationBackend):
141
+ """Object-level authorization: a user may only touch threads they own.
142
+
143
+ This is the built-in "secure" backend. It enforces the rule most multi-tenant
144
+ apps want without the developer writing any code: *a thread can be read, mutated,
145
+ stopped or fixed only by the user who owns it.* Ownership is resolved from the
146
+ configured checkpointer, whose thread registry records the owning ``user_id``.
147
+
148
+ Decision table (for the thread-scoped resources ``graph`` and ``checkpointer``):
149
+
150
+ - No ``user_id`` on the request -> deny (must be authenticated).
151
+ - No ``resource_id`` (list / create-without-id, e.g. ``GET /v1/threads``, the graph
152
+ schema endpoint) -> allow; those paths are already user-scoped by the service.
153
+ - The thread does not exist yet -> allow (a brand-new session / empty read). For
154
+ ``invoke``/``stream`` this starts a new thread owned by the caller.
155
+ - The thread exists and the caller owns it -> allow.
156
+ - The thread exists and is owned by someone else -> **deny, for every action**
157
+ including ``invoke`` and ``stream``. This is the whole point: you cannot read,
158
+ continue, stop, fix, or run another user's thread.
159
+
160
+ Resources other than ``graph``/``checkpointer`` (store, files) are allowed through:
161
+ they enforce their own per-user scoping (the store keys on ``user_id``; media checks
162
+ file ownership in ``MediaService``).
163
+
164
+ Ownership is resolved through a :class:`ThreadOwnershipResolver` in front of
165
+ :meth:`BaseCheckpointer.aget_thread_owner`. Ownership is immutable, so the resolver
166
+ caches it (in-process, plus optional shared Redis) -- after the first lookup a check is
167
+ an in-memory hit, not a database round-trip per request. A checkpointer that cannot
168
+ resolve ownership (raises ``NotImplementedError``) or that is not configured leaves
169
+ nothing to enforce, so requests pass through with a warning. The in-memory checkpointer
170
+ only records a thread when one is explicitly written, which is why this backend is the
171
+ production default (Postgres) while development defaults to
172
+ :class:`DefaultAuthorizationBackend`.
173
+ """
174
+
175
+ # Resources whose ``resource_id`` is a thread_id and therefore ownership-checkable.
176
+ THREAD_RESOURCES = frozenset({"graph", "checkpointer"})
177
+
178
+ def isolation_scope(self) -> str:
179
+ # Owner-only access -> storage must be scoped to the caller too.
180
+ return "owner"
181
+
182
+ def __init__(
183
+ self,
184
+ checkpointer: Any | None = None,
185
+ *,
186
+ resolver: Any | None = None,
187
+ redis: object | None = None,
188
+ ) -> None:
189
+ self._checkpointer = checkpointer
190
+ self._resolver = resolver
191
+ self._redis = redis
192
+ self._resolver_built = resolver is not None
193
+
194
+ @property
195
+ def checkpointer(self) -> Any | None:
196
+ """Resolve the checkpointer lazily from the DI container if not injected."""
197
+ if self._checkpointer is None:
198
+ try:
199
+ from injectq import InjectQ
200
+ from tenxgraph.storage.checkpointer import BaseCheckpointer
201
+
202
+ self._checkpointer = InjectQ.get_instance().try_get(BaseCheckpointer)
203
+ except Exception: # pragma: no cover - defensive
204
+ self._checkpointer = None
205
+ return self._checkpointer
206
+
207
+ def _get_resolver(self) -> Any | None:
208
+ """Return the cached ownership resolver, building it once from the checkpointer.
209
+
210
+ Returns None when no checkpointer is available (nothing to resolve).
211
+ """
212
+ if self._resolver_built:
213
+ return self._resolver
214
+ self._resolver_built = True
215
+ cp = self.checkpointer
216
+ if cp is None:
217
+ self._resolver = None
218
+ else:
219
+ from tenxgraph_api.src.app.core.auth.ownership_resolver import (
220
+ ThreadOwnershipResolver,
221
+ )
222
+
223
+ self._resolver = ThreadOwnershipResolver(cp.aget_thread_owner, redis=self._redis)
224
+ return self._resolver
225
+
226
+ def evict(self, thread_id: str | int) -> Any:
227
+ """Invalidate a thread's cached ownership (call on thread deletion).
228
+
229
+ Returns the resolver's ``evict`` coroutine when a resolver exists, so callers may
230
+ ``await`` it; returns None when there is nothing to evict.
231
+ """
232
+ resolver = self._get_resolver()
233
+ if resolver is None:
234
+ return None
235
+ return resolver.evict(thread_id)
236
+
237
+ async def aclose(self) -> None:
238
+ """Close the L2 Redis client this backend created (called on app shutdown)."""
239
+ client = self._redis
240
+ if client is not None and hasattr(client, "aclose"):
241
+ try:
242
+ await client.aclose()
243
+ except Exception as exc: # pragma: no cover - best-effort shutdown
244
+ logger.debug("Ownership Redis client close failed: %s", exc)
245
+
246
+ async def authorize(
247
+ self,
248
+ user: dict[str, Any],
249
+ resource: str,
250
+ action: str,
251
+ resource_id: str | None = None,
252
+ **context: Any,
253
+ ) -> bool:
254
+ user_id = user.get("user_id") if user else None
255
+ if not user_id:
256
+ return False
257
+
258
+ # Only thread-scoped resources are ownership-checked here.
259
+ if resource not in self.THREAD_RESOURCES:
260
+ return True
261
+
262
+ # Endpoints without a specific thread (list/create) are user-scoped by the
263
+ # service layer; nothing to authorize at the object level.
264
+ if resource_id is None:
265
+ return True
266
+
267
+ resolver = self._get_resolver()
268
+ if resolver is None:
269
+ logger.warning(
270
+ "Ownership authorization is active but no checkpointer is configured; "
271
+ "allowing %s:%s (no persisted threads to protect).",
272
+ resource,
273
+ action,
274
+ )
275
+ return True
276
+
277
+ return await self._authorize_ownership(
278
+ resolver, str(resource_id), str(user_id), resource, action
279
+ )
280
+
281
+ async def _authorize_ownership(
282
+ self,
283
+ resolver: Any,
284
+ resource_id: str,
285
+ user_id: str,
286
+ resource: str,
287
+ action: str,
288
+ ) -> bool:
289
+ try:
290
+ owner = await resolver.owner_of(resource_id)
291
+ except NotImplementedError:
292
+ logger.warning(
293
+ "Checkpointer %s cannot resolve thread ownership; ownership "
294
+ "authorization is inactive for %s:%s. Use a checkpointer that "
295
+ "implements aget_thread_owner (e.g. PgCheckpointer) or configure a "
296
+ "custom AuthorizationBackend.",
297
+ type(self.checkpointer).__name__,
298
+ resource,
299
+ action,
300
+ )
301
+ return True
302
+ except Exception as exc:
303
+ # Fail closed: a resolution error must not silently grant access.
304
+ logger.warning(
305
+ "Ownership check failed for thread %s (user %s): %s; denying.",
306
+ resource_id,
307
+ user_id,
308
+ exc,
309
+ )
310
+ return False
311
+
312
+ # Unknown thread -> brand-new session (or an empty read); allow. Otherwise the
313
+ # caller must be the owner, for EVERY action (invoke/stream included).
314
+ if owner is None:
315
+ return True
316
+ return str(owner) == user_id
317
+
318
+
319
+ class RoleBasedAuthorizationBackend(OwnershipAuthorizationBackend):
320
+ """RBAC: map a user's roles to scopes, on top of owner-only object isolation.
321
+
322
+ Combines two things in one backend:
323
+
324
+ - **Scopes** (what actions are allowed) -- ``scopes_for`` maps ``user["roles"]`` (or
325
+ ``user["role"]``) to a set of scopes via the ``role_scopes`` table. ``RequirePermission``
326
+ then enforces the required ``"<resource>:<action>"`` scope. A role granting ``"*"``
327
+ gets every scope in :func:`all_scopes`.
328
+ - **Isolation** (which rows are visible) -- inherited from
329
+ :class:`OwnershipAuthorizationBackend` (owner-only threads), configurable via
330
+ ``isolation``.
331
+
332
+ Example::
333
+
334
+ RoleBasedAuthorizationBackend(
335
+ role_scopes={
336
+ "admin": ["*"],
337
+ "member": ["graph:invoke", "graph:stream", "checkpointer:read"],
338
+ },
339
+ default_scopes=["graph:read"], # granted to everyone, even with no role
340
+ )
341
+ """
342
+
343
+ def __init__(
344
+ self,
345
+ role_scopes: dict[str, Any] | None = None,
346
+ *,
347
+ default_scopes: Any = (),
348
+ isolation: str = "owner",
349
+ checkpointer: Any | None = None,
350
+ resolver: Any | None = None,
351
+ redis: object | None = None,
352
+ ) -> None:
353
+ super().__init__(checkpointer=checkpointer, resolver=resolver, redis=redis)
354
+ self._role_scopes = {str(r): list(s) for r, s in (role_scopes or {}).items()}
355
+ self._default_scopes = list(default_scopes)
356
+ self._isolation = isolation if isolation in ("owner", "none") else "owner"
357
+
358
+ def isolation_scope(self) -> str:
359
+ return self._isolation
360
+
361
+ @staticmethod
362
+ def _roles_of(user: dict[str, Any]) -> list[str]:
363
+ if not isinstance(user, dict):
364
+ return []
365
+ roles = user.get("roles")
366
+ if roles is None:
367
+ role = user.get("role")
368
+ roles = [role] if role else []
369
+ elif isinstance(roles, str):
370
+ roles = [roles]
371
+ return [str(r) for r in roles]
372
+
373
+ def scopes_for(self, user: dict[str, Any]) -> list[str]:
374
+ granted: set[str] = set(self._default_scopes)
375
+ for role in self._roles_of(user):
376
+ granted.update(self._role_scopes.get(role, []))
377
+ if "*" in granted:
378
+ return sorted(all_scopes())
379
+ return sorted(granted)
@@ -0,0 +1,27 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+ from fastapi import Request, Response
5
+ from fastapi.security import HTTPAuthorizationCredentials
6
+
7
+
8
+ class BaseAuth(ABC):
9
+ @abstractmethod
10
+ def authenticate(
11
+ self,
12
+ request: Request,
13
+ response: Response,
14
+ credential: HTTPAuthorizationCredentials,
15
+ ) -> dict[str, Any] | None:
16
+ """Authenticate the user based on the provided credentials.
17
+ IT should return an empty dict if no authentication is required.
18
+ If authentication fails, it should raise an appropriate exception.
19
+ In case authentication is successful, it should return a dict with user information,
20
+ containing at least a 'user_id' key, and optionally other user details.
21
+ Example:
22
+ return {"user_id": "12345", "username": "johndoe", "email": "johndoe@example.com"}
23
+
24
+ What ever keys are returned that will be merged with config in the main graph app.
25
+ """
26
+
27
+ raise NotImplementedError