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,516 @@
1
+ import importlib
2
+ import inspect
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+
7
+ from injectq import InjectQ
8
+ from tenxgraph.core import CompiledGraph
9
+ from tenxgraph.storage.checkpointer import BaseCheckpointer
10
+ from tenxgraph.storage.store import BaseStore
11
+
12
+ from tenxgraph_api import BaseAuth
13
+ from tenxgraph_api.src.app.core.auth.authorization import (
14
+ AuthorizationBackend,
15
+ DefaultAuthorizationBackend,
16
+ OwnershipAuthorizationBackend,
17
+ RoleBasedAuthorizationBackend,
18
+ )
19
+ from tenxgraph_api.src.app.core.config.graph_config import GraphConfig
20
+ from tenxgraph_api.src.app.utils.thread_name_generator import ThreadNameGenerator
21
+
22
+
23
+ logger = logging.getLogger("tenxgraph_api.loader")
24
+
25
+
26
+ async def load_graph(path: str) -> CompiledGraph | None:
27
+ if ":" not in path:
28
+ raise ValueError(f"Invalid graph path format '{path}'. Expected 'module:attribute'.")
29
+
30
+ module_name_importable, function_name = path.split(":")
31
+
32
+ try:
33
+ module = importlib.import_module(module_name_importable)
34
+ entry_point_obj = getattr(module, function_name)
35
+
36
+ if callable(entry_point_obj):
37
+ if inspect.iscoroutinefunction(entry_point_obj):
38
+ app = await entry_point_obj()
39
+ else:
40
+ app = entry_point_obj()
41
+ else:
42
+ app = entry_point_obj
43
+
44
+ if app is None:
45
+ raise RuntimeError(f"Failed to obtain a runnable graph from {path}.")
46
+
47
+ if isinstance(app, CompiledGraph):
48
+ logger.info(f"Successfully loaded graph '{function_name}' from {path}.")
49
+ else:
50
+ raise TypeError("Loaded object is not a CompiledGraph.")
51
+
52
+ except ModuleNotFoundError as e:
53
+ logger.error(f"Module not found when loading graph from {path}: {e}")
54
+ raise ModuleNotFoundError(f"Module not found for graph path '{path}': {e}")
55
+ except AttributeError as e:
56
+ logger.error(f"Attribute not found when loading graph from {path}: {e}")
57
+ raise AttributeError(f"Attribute not found for graph path '{path}': {e}")
58
+ except Exception as e:
59
+ logger.error(f"Error loading graph from {path}: {e}")
60
+ raise Exception(f"Failed to load graph from {path}: {e}")
61
+
62
+ return app
63
+
64
+
65
+ def load_checkpointer(path: str | None) -> BaseCheckpointer | None:
66
+ if not path:
67
+ return None
68
+
69
+ if ":" not in path:
70
+ raise ValueError(f"Invalid checkpointer path format '{path}'. Expected 'module:attribute'.")
71
+
72
+ module_name_importable, function_name = path.split(":")
73
+
74
+ try:
75
+ module = importlib.import_module(module_name_importable)
76
+ entry_point_obj = getattr(module, function_name)
77
+ checkpointer = entry_point_obj
78
+
79
+ if checkpointer is None:
80
+ raise RuntimeError(f"Failed to obtain a BaseCheckpointer graph from {path}.")
81
+
82
+ if isinstance(checkpointer, BaseCheckpointer):
83
+ logger.info(f"Successfully loaded BaseCheckpointer '{function_name}' from {path}.")
84
+ else:
85
+ raise TypeError("Loaded object is not a BaseCheckpointer.")
86
+ except ModuleNotFoundError as e:
87
+ logger.error(f"Module not found when loading BaseCheckpointer from {path}: {e}")
88
+ raise ModuleNotFoundError(f"Module not found for checkpointer path '{path}': {e}")
89
+ except AttributeError as e:
90
+ logger.error(f"Attribute not found when loading BaseCheckpointer from {path}: {e}")
91
+ raise AttributeError(f"Attribute not found for checkpointer path '{path}': {e}")
92
+ except Exception as e:
93
+ logger.error(f"Error loading BaseCheckpointer from {path}: {e}")
94
+ raise Exception(f"Failed to load BaseCheckpointer from {path}: {e}")
95
+
96
+ return checkpointer
97
+
98
+
99
+ def load_store(path: str | None) -> BaseStore | None:
100
+ if not path:
101
+ return None
102
+
103
+ if ":" not in path:
104
+ raise ValueError(f"Invalid store path format '{path}'. Expected 'module:attribute'.")
105
+
106
+ module_name_importable, function_name = path.split(":")
107
+
108
+ try:
109
+ module = importlib.import_module(module_name_importable)
110
+ entry_point_obj = getattr(module, function_name)
111
+ store = entry_point_obj
112
+
113
+ if store is None:
114
+ raise RuntimeError(f"Failed to obtain a BaseStore from {path}.")
115
+
116
+ if isinstance(store, BaseStore):
117
+ logger.info(f"Successfully loaded graph '{function_name}' from {path}.")
118
+ else:
119
+ raise TypeError("Loaded object is not a BaseStore.")
120
+ except ModuleNotFoundError as e:
121
+ logger.error(f"Module not found when loading BaseStore from {path}: {e}")
122
+ raise ModuleNotFoundError(f"Module not found for store path '{path}': {e}")
123
+ except AttributeError as e:
124
+ logger.error(f"Attribute not found when loading BaseStore from {path}: {e}")
125
+ raise AttributeError(f"Attribute not found for store path '{path}': {e}")
126
+ except Exception as e:
127
+ logger.error(f"Error loading BaseStore from {path}: {e}")
128
+ raise Exception(f"Failed to load BaseStore from {path}: {e}")
129
+
130
+ return store
131
+
132
+
133
+ def load_container(path: str | None) -> InjectQ | None:
134
+ if not path:
135
+ return None
136
+
137
+ module_name_importable, function_name = path.split(":")
138
+
139
+ try:
140
+ module = importlib.import_module(module_name_importable)
141
+ entry_point_obj = getattr(module, function_name)
142
+ container = entry_point_obj
143
+
144
+ if container is None:
145
+ raise RuntimeError(f"Failed to obtain a InjectQ from {path}.")
146
+
147
+ if isinstance(container, InjectQ):
148
+ logger.info(f"Successfully loaded InjectQ '{function_name}' from {path}.")
149
+ else:
150
+ raise TypeError("Loaded object is not a InjectQ.")
151
+ except Exception as e:
152
+ logger.error(f"Error loading InjectQ from {path}: {e}")
153
+ raise Exception(f"Failed to load InjectQ from {path}: {e}")
154
+
155
+ # if we have container, set it as the global instance
156
+ if container:
157
+ container.activate()
158
+
159
+ return container
160
+
161
+
162
+ def load_auth(path: str | None) -> BaseAuth | None:
163
+ if not path:
164
+ return None
165
+
166
+ if ":" not in path:
167
+ raise ValueError(f"Invalid auth path format '{path}'. Expected 'module:attribute'.")
168
+
169
+ module_name_importable, function_name = path.split(":")
170
+
171
+ try:
172
+ module = importlib.import_module(module_name_importable)
173
+ entry_point_obj = getattr(module, function_name)
174
+
175
+ if inspect.isclass(entry_point_obj) and issubclass(entry_point_obj, BaseAuth):
176
+ auth = entry_point_obj()
177
+ elif isinstance(entry_point_obj, BaseAuth):
178
+ auth = entry_point_obj
179
+ else:
180
+ raise TypeError("Loaded object is not a subclass or instance of BaseAuth.")
181
+
182
+ logger.info(f"Successfully loaded BaseAuth '{function_name}' from {path}.")
183
+ except ModuleNotFoundError as e:
184
+ logger.error(f"Module not found when loading BaseAuth from {path}: {e}")
185
+ raise ModuleNotFoundError(f"Module not found for auth path '{path}': {e}")
186
+ except AttributeError as e:
187
+ logger.error(f"Attribute not found when loading BaseAuth from {path}: {e}")
188
+ raise AttributeError(f"Attribute not found for auth path '{path}': {e}")
189
+ except Exception as e:
190
+ logger.error(f"Error loading BaseAuth from {path}: {e}")
191
+ raise Exception(f"Failed to load BaseAuth from {path}: {e}")
192
+
193
+ return auth
194
+
195
+
196
+ def load_authorization(path: str | None) -> AuthorizationBackend | None:
197
+ """
198
+ Load authorization backend from the specified path.
199
+
200
+ Args:
201
+ path: Module path in format 'module:attribute' or None
202
+
203
+ Returns:
204
+ AuthorizationBackend instance or None if path is not provided
205
+
206
+ Raises:
207
+ Exception: If loading fails or object is not AuthorizationBackend
208
+ """
209
+ if not path:
210
+ return None
211
+
212
+ module_name_importable, function_name = path.split(":")
213
+
214
+ try:
215
+ module = importlib.import_module(module_name_importable)
216
+ entry_point_obj = getattr(module, function_name)
217
+
218
+ # If it's a class, instantiate it; if it's an instance, use as is
219
+ if inspect.isclass(entry_point_obj) and issubclass(entry_point_obj, AuthorizationBackend):
220
+ authorization = entry_point_obj()
221
+ elif isinstance(entry_point_obj, AuthorizationBackend):
222
+ authorization = entry_point_obj
223
+ else:
224
+ raise TypeError("Loaded object is not a subclass or instance of AuthorizationBackend.")
225
+
226
+ logger.info(f"Successfully loaded AuthorizationBackend '{function_name}' from {path}.")
227
+ except Exception as e:
228
+ logger.error(f"Error loading AuthorizationBackend from {path}: {e}")
229
+ raise Exception(f"Failed to load AuthorizationBackend from {path}: {e}")
230
+
231
+ return authorization
232
+
233
+
234
+ def load_thread_name_generator(path: str | None) -> ThreadNameGenerator | None:
235
+ if not path:
236
+ return None
237
+
238
+ module_name_importable, function_name = path.split(":")
239
+
240
+ try:
241
+ module = importlib.import_module(module_name_importable)
242
+ entry_point_obj = getattr(module, function_name)
243
+
244
+ # If it's a class, instantiate it; if it's an instance, use as is
245
+ if inspect.isclass(entry_point_obj) and issubclass(entry_point_obj, ThreadNameGenerator):
246
+ thread_name_generator = entry_point_obj()
247
+ elif isinstance(entry_point_obj, ThreadNameGenerator):
248
+ thread_name_generator = entry_point_obj
249
+ else:
250
+ raise TypeError("Loaded object is not a subclass or instance of ThreadNameGenerator.")
251
+
252
+ logger.info(f"Successfully loaded ThreadNameGenerator '{function_name}' from {path}.")
253
+ except Exception as e:
254
+ logger.error(f"Error loading ThreadNameGenerator from {path}: {e}")
255
+ raise Exception(f"Failed to load ThreadNameGenerator from {path}: {e}")
256
+
257
+ return thread_name_generator
258
+
259
+
260
+ def load_and_bind_auth(container: InjectQ, auth_config: dict) -> None:
261
+ """Bind the authentication backend described by ``GraphConfig.auth_config()``.
262
+
263
+ ``jwt`` needs no path; ``custom`` needs a ``path`` to a ``BaseAuth``. Any other method
264
+ is a configuration error and fails at startup rather than silently binding no backend.
265
+ """
266
+ from tenxgraph_api.src.app.core.auth.jwt_auth import JwtAuth, check_jwt_settings
267
+ from tenxgraph_api.src.app.core.config.settings import get_settings
268
+
269
+ method = auth_config.get("method")
270
+ if method == "jwt":
271
+ check_jwt_settings(get_settings())
272
+ auth_backend: BaseAuth | None = JwtAuth()
273
+ elif method == "none":
274
+ auth_backend = None
275
+ elif method == "custom":
276
+ auth_backend = _load_custom_auth(auth_config.get("path"))
277
+ else:
278
+ raise ValueError(f"Unsupported auth method: {method!r}. Use 'jwt' or 'custom'.")
279
+
280
+ container.bind_instance(BaseAuth, auth_backend, allow_none=True)
281
+
282
+
283
+ def _load_custom_auth(path: str | None) -> BaseAuth:
284
+ if not path:
285
+ raise ValueError("Custom auth requires a 'path' in auth_config.")
286
+
287
+ # Extract file path before the ':' for existence check
288
+ module_or_path = path.split(":", 1)[0] if ":" in path else path
289
+
290
+ # Simple handling: if it appears to be a filesystem path, use it; otherwise
291
+ # convert dotted module path to a file path like src/auth/custom_auth.py
292
+ if os.path.sep in module_or_path or module_or_path.endswith(".py"):
293
+ file_path = Path(module_or_path)
294
+ elif "." in module_or_path and os.path.sep not in module_or_path:
295
+ file_path = Path(module_or_path.replace(".", os.path.sep) + ".py")
296
+ else:
297
+ file_path = Path(module_or_path)
298
+
299
+ if not file_path.exists():
300
+ raise ValueError(f"Custom auth path does not exist: {module_or_path}")
301
+
302
+ auth = load_auth(path)
303
+ if auth is None: # load_auth only returns None for an empty path, checked above
304
+ raise ValueError("Custom auth requires a 'path' in auth_config.")
305
+ return auth
306
+
307
+
308
+ # Built-in authorization backends selectable by name in ``10xgraph.json``.
309
+ _BUILTIN_AUTHORIZATION = {
310
+ "ownership": OwnershipAuthorizationBackend, # object-level: owner-only thread access
311
+ "allow_all": DefaultAuthorizationBackend, # authenticated == authorized
312
+ "default": DefaultAuthorizationBackend, # alias for allow_all
313
+ "none": DefaultAuthorizationBackend, # alias for allow_all
314
+ }
315
+
316
+
317
+ def _build_ownership_redis(redis_url: str | None) -> object | None:
318
+ """Build an async Redis client for the ownership L2 cache, or None.
319
+
320
+ Returns None (L1-only) when no URL is configured or the ``redis`` package is absent;
321
+ the resolver degrades gracefully either way.
322
+ """
323
+ if not redis_url:
324
+ return None
325
+ try:
326
+ from redis.asyncio import Redis as AsyncRedis # type: ignore[import]
327
+ except ImportError:
328
+ logger.warning(
329
+ "REDIS configured but the 'redis' package is not installed; ownership cache "
330
+ "runs in-process only (L1). Install the redis extra to share it across workers."
331
+ )
332
+ return None
333
+ try:
334
+ client = AsyncRedis.from_url(redis_url, decode_responses=False)
335
+ logger.info("Ownership authorization L2 cache enabled (shared Redis).")
336
+ return client
337
+ except Exception as exc: # pragma: no cover - defensive
338
+ logger.warning("Failed to build ownership Redis client: %s; using L1 cache only.", exc)
339
+ return None
340
+
341
+
342
+ def _resolve_authorization_backend(
343
+ authorization: str | dict | None, redis_url: str | None = None
344
+ ) -> AuthorizationBackend:
345
+ """Pick the authorization backend from config, defaulting by run mode.
346
+
347
+ Resolution order (developer choice always wins):
348
+
349
+ 1. A dict ``{"backend": "rbac", "roles": {...}, ...}`` -> role-based access control.
350
+ 2. ``"module:attr"`` -> the developer's custom :class:`AuthorizationBackend`.
351
+ 3. A built-in name (``"ownership"``, ``"allow_all"``/``"default"``/``"none"``) ->
352
+ that backend, regardless of mode.
353
+ 4. Not configured (``null``) -> secure-by-default in production
354
+ (:class:`OwnershipAuthorizationBackend`), permissive in development
355
+ (:class:`DefaultAuthorizationBackend`). Either can be overridden via (1)/(2)/(3).
356
+
357
+ ``redis_url`` (when set) backs the ownership cache's shared L2 tier.
358
+ """
359
+ # 1. Config-driven RBAC: {"backend": "rbac", "roles": {...}, "default_scopes": [...]}
360
+ if isinstance(authorization, dict):
361
+ backend_name = (authorization.get("backend") or authorization.get("type") or "").lower()
362
+ if backend_name in ("rbac", "role_based", "roles"):
363
+ logger.info("Using RoleBasedAuthorizationBackend (config-driven roles).")
364
+ return RoleBasedAuthorizationBackend(
365
+ role_scopes=authorization.get("roles") or authorization.get("role_scopes") or {},
366
+ default_scopes=authorization.get("default_scopes") or (),
367
+ isolation=authorization.get("isolation", "owner"),
368
+ redis=_build_ownership_redis(redis_url),
369
+ )
370
+ raise ValueError(
371
+ f"Unknown authorization backend config: {authorization!r}. For RBAC use "
372
+ '{"backend": "rbac", "roles": {...}}.'
373
+ )
374
+
375
+ if authorization and ":" in authorization:
376
+ backend = load_authorization(authorization)
377
+ logger.info("Using custom AuthorizationBackend from '%s'.", authorization)
378
+ return backend # type: ignore[return-value]
379
+
380
+ if authorization:
381
+ key = authorization.strip().lower()
382
+ builtin = _BUILTIN_AUTHORIZATION.get(key)
383
+ if builtin is None:
384
+ raise ValueError(
385
+ f"Unknown authorization backend '{authorization}'. Use a 'module:attr' "
386
+ f"path or one of: {', '.join(sorted(_BUILTIN_AUTHORIZATION))}."
387
+ )
388
+ logger.info("Using built-in '%s' authorization backend.", key)
389
+ if builtin is OwnershipAuthorizationBackend:
390
+ return OwnershipAuthorizationBackend(redis=_build_ownership_redis(redis_url))
391
+ return builtin()
392
+
393
+ # Not configured: default by mode.
394
+ from tenxgraph_api.src.app.core.config.settings import get_settings
395
+
396
+ if get_settings().MODE == "production":
397
+ logger.info(
398
+ "No authorization configured; defaulting to OwnershipAuthorizationBackend "
399
+ "(owner-only thread access) because MODE=production. Set "
400
+ '"authorization": "allow_all" to opt out.'
401
+ )
402
+ return OwnershipAuthorizationBackend(redis=_build_ownership_redis(redis_url))
403
+
404
+ logger.info(
405
+ "No authorization configured; defaulting to DefaultAuthorizationBackend "
406
+ "(allows all authenticated users) in development. Set "
407
+ '"authorization": "ownership" to enforce owner-only access here too.'
408
+ )
409
+ return DefaultAuthorizationBackend()
410
+
411
+
412
+ def load_and_bind_authorization(
413
+ container: InjectQ,
414
+ authorization_path: str | None,
415
+ redis_url: str | None = None,
416
+ ) -> None:
417
+ backend = _resolve_authorization_backend(authorization_path, redis_url)
418
+ container.bind_instance(AuthorizationBackend, backend)
419
+
420
+
421
+ async def attach_all_modules(
422
+ config: GraphConfig,
423
+ container: InjectQ,
424
+ ) -> CompiledGraph | None:
425
+ remote_tools = config.remote_tools
426
+ graph = await load_graph(config.graph_path)
427
+
428
+ if remote_tools:
429
+ if graph is None:
430
+ raise RuntimeError("Cannot attach remote tools because the graph failed to load")
431
+ grouped: dict[str, list[dict]] = {}
432
+ for tool in remote_tools:
433
+ grouped.setdefault(tool.node_name, []).append(tool.to_tool_schema())
434
+ for node_name, schemas in grouped.items():
435
+ graph.attach_remote_tools(schemas, node_name)
436
+ logger.info("Attached %d configured remote tools", len(remote_tools))
437
+
438
+ logger.info("All modules attached successfully")
439
+
440
+ # This binding we have done already in the library
441
+ # # Bind checkpointer instance if configured
442
+ # checkpointer = load_checkpointer(config.checkpointer_path)
443
+ # container.bind_instance(BaseCheckpointer, checkpointer, allow_none=True)
444
+
445
+ # Bind the store instance (if configured) so the /v1/store endpoints — which
446
+ # inject BaseStore via StoreService — can serve it. Without this the store
447
+ # router reports "Store is not configured" even when 10xgraph.json sets one.
448
+ store = load_store(config.store_path)
449
+ container.bind_instance(BaseStore, store, allow_none=True)
450
+
451
+ # Bind the in-memory telemetry store so the graph service can record run
452
+ # events and the /v1/observability endpoints can reconstruct traces.
453
+ #
454
+ # This is a dev-only convenience: it lets the playground show a trace without
455
+ # any external observability backend. It is NOT durable and NOT meant for
456
+ # production (use OTEL / a publisher there instead). So we only bind it
457
+ # outside production; in production the store stays unbound and the
458
+ # /v1/observability endpoints return a clean, empty "disabled" response.
459
+ from tenxgraph_api.src.app.core.config.settings import get_settings
460
+ from tenxgraph_api.src.app.utils.telemetry_store import TelemetryStore
461
+
462
+ if get_settings().MODE != "production":
463
+ container.bind_instance(TelemetryStore, TelemetryStore())
464
+ logger.info("Local in-memory telemetry store bound (dev mode)")
465
+ else:
466
+ logger.info("Production mode: local telemetry store disabled (use OTEL/publisher)")
467
+
468
+ # load auth backend
469
+ auth_config = config.auth_config()
470
+ if auth_config:
471
+ load_and_bind_auth(container, auth_config)
472
+ else:
473
+ # Auth disabled is a legitimate choice (single-user / trusted network), but it
474
+ # must not be silent -- an operator who simply forgot to set `auth` should see
475
+ # it at startup. This warns; it does not force auth on.
476
+ logger.warning(
477
+ "⚠️ Authentication is DISABLED (no `auth` configured). Every request runs "
478
+ "as `anonymous` with no access control. This is fine for single-user or "
479
+ "trusted-network deployments; set `auth` in 10xgraph.json to enable it."
480
+ )
481
+ # bind None
482
+ container.bind_instance(BaseAuth, None, allow_none=True)
483
+
484
+ # load thread name generator
485
+ thread_name_generator_path = config.thread_name_generator_path
486
+ if thread_name_generator_path:
487
+ thread_name_generator = load_thread_name_generator(thread_name_generator_path)
488
+ container.bind_instance(ThreadNameGenerator, thread_name_generator)
489
+ else:
490
+ # bind None if not configured
491
+ container.bind_instance(ThreadNameGenerator, None, allow_none=True)
492
+
493
+ # load authorization backend. The ownership cache's shared L2 tier reuses the
494
+ # configured Redis URL (config `redis`, else settings.REDIS_URL); absent -> L1 only.
495
+ from tenxgraph_api.src.app.core.config.settings import get_settings
496
+
497
+ authorization_path = config.authorization_path
498
+ ownership_redis_url = config.redis_url or get_settings().REDIS_URL
499
+ load_and_bind_authorization(container, authorization_path, ownership_redis_url)
500
+
501
+ # --- Media service wiring ---
502
+ from tenxgraph_api.src.app.core.config.media_settings import (
503
+ MediaSettings,
504
+ get_media_settings,
505
+ )
506
+ from tenxgraph_api.src.app.routers.media import MediaService
507
+
508
+ media_settings = get_media_settings()
509
+ container.bind_instance(MediaSettings, media_settings)
510
+ media_service = MediaService(settings=media_settings)
511
+ container.bind_instance(MediaService, media_service)
512
+
513
+ logger.info("Container loaded successfully")
514
+ logger.debug(f"Container dependency graph: {container.get_dependency_graph()}")
515
+
516
+ return graph
@@ -0,0 +1,180 @@
1
+ import logging
2
+ import os
3
+
4
+ from fastapi import FastAPI
5
+ from fastapi.concurrency import asynccontextmanager
6
+ from fastapi.responses import ORJSONResponse
7
+ from injectq import InjectQ
8
+ from injectq.integrations.fastapi import setup_fastapi
9
+ from tenxgraph.core.graph import CompiledGraph
10
+ from tenxgraph.utils.background_task_manager import BackgroundTaskManager
11
+
12
+ # # Prometheus Instrumentator import
13
+ # from prometheus_fastapi_instrumentator import Instrumentator
14
+ # from tortoise import Tortoise
15
+ from tenxgraph_api.src.app.core import (
16
+ get_settings,
17
+ init_errors_handler,
18
+ init_logger,
19
+ setup_middleware,
20
+ )
21
+ from tenxgraph_api.src.app.core.auth.route_guard import assert_all_routes_protected
22
+ from tenxgraph_api.src.app.core.config.graph_config import GraphConfig, default_config_path
23
+ from tenxgraph_api.src.app.loader import attach_all_modules, load_container
24
+ from tenxgraph_api.src.app.routers import init_routes
25
+
26
+
27
+ logger = logging.getLogger("tenxgraph_api")
28
+
29
+ # How long shutdown waits for in-flight background work (event publishing,
30
+ # checkpoint writes) before giving up. Bounded on purpose: a wedged sink must not
31
+ # hold the pod open until the kubelet SIGKILLs it, which would lose more than
32
+ # draining for a fixed window and then moving on.
33
+ SHUTDOWN_DRAIN_TIMEOUT_SECONDS = 30.0
34
+
35
+ # GraphConfig loads the ``env`` file named in 10xgraph.json, so it must run before Settings
36
+ # is first built. `10xgraph api` loads that file itself, but plain `gunicorn ...main:app`
37
+ # (the generated Dockerfile) does not, and Settings is cached after the first read.
38
+ graph_path = os.environ.get("GRAPH_PATH") or default_config_path()
39
+ graph_config = GraphConfig(graph_path)
40
+
41
+ settings = get_settings()
42
+ # Load the container
43
+ container: InjectQ = load_container(graph_config.injectq_path) or InjectQ.get_instance()
44
+
45
+ # Save config instance
46
+ container.bind_instance(GraphConfig, graph_config)
47
+
48
+
49
+ async def _cleanup_temp_media_cache() -> None:
50
+ """Run best-effort cleanup of expired temporary media cache entries."""
51
+ try:
52
+ from tenxgraph.storage.media.temp_cache import TemporaryMediaCache
53
+
54
+ checkpointer = container.try_get("checkpointer") or container.try_get("BaseCheckpointer")
55
+ media_store = container.try_get("media_store") or container.try_get("BaseMediaStore")
56
+
57
+ if checkpointer is None:
58
+ logger.debug("No checkpointer available, skipping temp media cache cleanup")
59
+ return
60
+
61
+ cache = TemporaryMediaCache()
62
+ cleaned = await cache.cleanup(checkpointer, media_store)
63
+ if cleaned:
64
+ logger.info("Cleaned up %d expired temporary media cache entries on startup", cleaned)
65
+ else:
66
+ logger.debug("No expired temporary media cache entries to clean up")
67
+ except Exception as e:
68
+ logger.warning("Failed to clean up temporary media cache on startup: %s", e)
69
+
70
+
71
+ @asynccontextmanager
72
+ async def lifespan(app: FastAPI):
73
+ # Load the cache
74
+ # RedisCacheBackend(settings.REDIS_URL)
75
+
76
+ graph: CompiledGraph | None = await attach_all_modules(
77
+ graph_config,
78
+ container=container,
79
+ )
80
+
81
+ # Clean up expired temporary media cache on startup
82
+ await _cleanup_temp_media_cache()
83
+
84
+ # load Store
85
+ # store = load_store(graph_config.store_path)
86
+ # injector.binder.bind(BaseStore, store)
87
+
88
+ yield
89
+
90
+ # Shutdown. Order matters: drain in-flight work BEFORE tearing down the
91
+ # resources that work depends on.
92
+ #
93
+ # Previously this went straight to `graph.aclose()`, which released the graph's
94
+ # resources while background work (publisher emits, checkpoint writes) could
95
+ # still be in flight -- so a rolling deploy could drop the tail end of a run's
96
+ # events and its final checkpoint.
97
+ await _drain_background_tasks()
98
+
99
+ if graph:
100
+ # release all the resources
101
+ await graph.aclose()
102
+
103
+ # Close rate-limit backend (e.g. Redis connection pool)
104
+ backend = getattr(app.state, "rate_limit_backend", None)
105
+ if backend is not None:
106
+ await backend.close()
107
+
108
+ # Close the authorization backend's L2 Redis client, if it created one.
109
+ try:
110
+ from tenxgraph_api.src.app.core.auth.authorization import AuthorizationBackend
111
+
112
+ authz = container.try_get(AuthorizationBackend)
113
+ if authz is not None and hasattr(authz, "aclose"):
114
+ await authz.aclose()
115
+ except Exception as exc:
116
+ logger.debug("Authorization backend close failed: %s", exc)
117
+
118
+
119
+ async def _drain_background_tasks(timeout: float = SHUTDOWN_DRAIN_TIMEOUT_SECONDS) -> None:
120
+ """Wait for in-flight background tasks (event publishing, checkpoint writes).
121
+
122
+ Bounded: if the sink is wedged we still shut down, rather than hanging the pod
123
+ until the kubelet SIGKILLs it -- which would lose strictly more than draining
124
+ for a bounded window and then giving up.
125
+ """
126
+ try:
127
+ task_manager = InjectQ.get_instance().try_get(BackgroundTaskManager)
128
+ except Exception: # container not configured; nothing to drain
129
+ return
130
+
131
+ if task_manager is None:
132
+ return
133
+
134
+ pending = getattr(task_manager, "pending_count", 0)
135
+ if not pending:
136
+ return
137
+
138
+ logger.info("Draining %d in-flight background tasks before shutdown", pending)
139
+ try:
140
+ await task_manager.wait_for_all(timeout=timeout)
141
+ logger.info("Background tasks drained")
142
+ except Exception as e:
143
+ logger.warning("Background task drain did not finish cleanly: %s", e)
144
+
145
+
146
+ app = FastAPI(
147
+ title=settings.APP_NAME,
148
+ version=settings.APP_VERSION,
149
+ debug=settings.MODE == "DEVELOPMENT",
150
+ summary=settings.SUMMARY,
151
+ docs_url=settings.DOCS_PATH if settings.DOCS_PATH else None,
152
+ redoc_url=settings.REDOCS_PATH if settings.REDOCS_PATH else None,
153
+ # The schema lists every route and model; serve it only alongside the docs.
154
+ openapi_url="/openapi.json" if settings.DOCS_PATH or settings.REDOCS_PATH else None,
155
+ default_response_class=ORJSONResponse,
156
+ lifespan=lifespan,
157
+ root_path=settings.ROOT_PATH,
158
+ )
159
+
160
+ setup_middleware(app, graph_config=graph_config, container=container)
161
+
162
+ # attach_injector(app, injector=injector)
163
+ setup_fastapi(container=container, app=app)
164
+
165
+ init_logger(settings.LOG_LEVEL)
166
+
167
+ # init error handler
168
+ init_errors_handler(app)
169
+
170
+ # init routes
171
+ # The evals viewer reads local report files and has no auth, so it is a development tool only.
172
+ # The AG-UI endpoint is opt-in through 10xgraph.json.
173
+ init_routes(app, evals=settings.MODE != "production", ag_ui=graph_config.ag_ui.enabled)
174
+
175
+ # Secure by construction: refuse to boot if any non-public route forgot its
176
+ # RequirePermission guard (a forgotten guard would otherwise ship an open endpoint).
177
+ assert_all_routes_protected(app)
178
+
179
+ # instrumentator = Instrumentator().instrument(app) # Instrument first
180
+ # instrumentator.expose(app) # Then expose