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,200 @@
1
+ """Store API schemas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+ from tenxgraph.core.state import Message
9
+ from tenxgraph.storage.store.store_schema import (
10
+ DistanceMetric,
11
+ MemoryRecord,
12
+ MemorySearchResult,
13
+ MemoryType,
14
+ RetrievalStrategy,
15
+ )
16
+
17
+
18
+ # Upper bounds so one request cannot make the store (and the embedding / vector backend)
19
+ # return or rank an unbounded number of rows.
20
+ MAX_SEARCH_LIMIT = 100
21
+ MAX_LIST_LIMIT = 1000
22
+ MAX_SEARCH_TOKENS = 16_000
23
+
24
+
25
+ class BaseConfigSchema(BaseModel):
26
+ """Base schema containing configuration overrides and store options."""
27
+
28
+ config: dict[str, Any] | None = Field(
29
+ default_factory=dict,
30
+ description="Configuration values forwarded to the store backend.",
31
+ )
32
+ options: dict[str, Any] | None = Field(
33
+ default=None,
34
+ description="Extra keyword arguments to forward to the store backend.",
35
+ )
36
+
37
+
38
+ class StoreMemorySchema(BaseConfigSchema):
39
+ """Schema for storing a memory item."""
40
+
41
+ content: str | Message = Field(..., description="Memory content or structured message.")
42
+ memory_type: MemoryType = Field(
43
+ default=MemoryType.EPISODIC,
44
+ description="Memory classification used by the backend store.",
45
+ )
46
+ category: str = Field(default="general", description="Category label for the memory.")
47
+ metadata: dict[str, Any] | None = Field(
48
+ default=None,
49
+ description="Arbitrary metadata associated with the memory.",
50
+ )
51
+
52
+
53
+ class SearchMemorySchema(BaseConfigSchema):
54
+ """Schema for searching memories."""
55
+
56
+ query: str = Field(..., description="Textual query used for memory retrieval.")
57
+ memory_type: MemoryType | None = Field(
58
+ default=None,
59
+ description="Optional memory type filter.",
60
+ )
61
+ category: str | None = Field(
62
+ default=None,
63
+ description="Optional category filter.",
64
+ )
65
+ limit: int = Field(
66
+ default=10,
67
+ gt=0,
68
+ le=MAX_SEARCH_LIMIT,
69
+ description="Maximum number of results to return.",
70
+ )
71
+ score_threshold: float | None = Field(
72
+ default=None,
73
+ description="Minimum similarity score required for results.",
74
+ )
75
+ filters: dict[str, Any] | None = Field(
76
+ default=None,
77
+ description="Additional store-specific filters.",
78
+ )
79
+ retrieval_strategy: RetrievalStrategy = Field(
80
+ default=RetrievalStrategy.SIMILARITY,
81
+ description="Retrieval strategy used by the backend store.",
82
+ )
83
+ distance_metric: DistanceMetric = Field(
84
+ default=DistanceMetric.COSINE,
85
+ description="Distance metric applied during similarity search.",
86
+ )
87
+ max_tokens: int = Field(
88
+ default=4000,
89
+ gt=0,
90
+ le=MAX_SEARCH_TOKENS,
91
+ description="Maximum tokens used for truncation in similarity search.",
92
+ )
93
+
94
+
95
+ class UpdateMemorySchema(BaseConfigSchema):
96
+ """Schema for updating a memory."""
97
+
98
+ content: str | Message = Field(..., description="Updated memory content or message.")
99
+ metadata: dict[str, Any] | None = Field(
100
+ default=None,
101
+ description="Updated metadata for the memory.",
102
+ )
103
+
104
+
105
+ class DeleteMemorySchema(BaseConfigSchema):
106
+ """Schema for deleting a memory."""
107
+
108
+
109
+ class GetMemorySchema(BaseConfigSchema):
110
+ """Schema for retrieving a single memory."""
111
+
112
+
113
+ class ListMemoriesSchema(BaseConfigSchema):
114
+ """Schema for listing memories."""
115
+
116
+ limit: int = Field(
117
+ default=100,
118
+ gt=0,
119
+ le=MAX_LIST_LIMIT,
120
+ description="Maximum number of memories to return.",
121
+ )
122
+
123
+
124
+ class ForgetMemorySchema(BaseConfigSchema):
125
+ """Schema for forgetting memories based on filters."""
126
+
127
+ memory_type: MemoryType | None = Field(
128
+ default=None,
129
+ description="Optional memory type to target for deletion.",
130
+ )
131
+ category: str | None = Field(
132
+ default=None,
133
+ description="Optional category to target for deletion.",
134
+ )
135
+ filters: dict[str, Any] | None = Field(
136
+ default=None,
137
+ description="Additional filters to control which memories are forgotten.",
138
+ )
139
+
140
+
141
+ class MemoryCreateResponseSchema(BaseModel):
142
+ """Response schema for create memory operations."""
143
+
144
+ memory_id: str = Field(..., description="Identifier of the stored memory.")
145
+
146
+
147
+ class MemoryItemResponseSchema(BaseModel):
148
+ """Response schema for single memory retrieval."""
149
+
150
+ memory: MemorySearchResult | None = Field(
151
+ default=None,
152
+ description="Memory retrieved from the store, if available.",
153
+ )
154
+
155
+
156
+ class MemoryListResponseSchema(BaseModel):
157
+ """Response schema for listing memories."""
158
+
159
+ memories: list[MemorySearchResult] = Field(
160
+ default_factory=list,
161
+ description="Collection of memories returned from the store.",
162
+ )
163
+
164
+
165
+ class MemorySearchResponseSchema(BaseModel):
166
+ """Response schema for search operations."""
167
+
168
+ results: list[MemorySearchResult] = Field(
169
+ default_factory=list,
170
+ description="Search results ranked by relevance.",
171
+ )
172
+
173
+
174
+ class MemoryOperationResponseSchema(BaseModel):
175
+ """Generic response schema for mutation operations."""
176
+
177
+ success: bool = Field(..., description="Whether the store operation succeeded.")
178
+ data: Any | None = Field(default=None, description="Optional payload returned by the store.")
179
+
180
+
181
+ __all__ = [
182
+ "BaseConfigSchema",
183
+ "DeleteMemorySchema",
184
+ "DistanceMetric",
185
+ "ForgetMemorySchema",
186
+ "GetMemorySchema",
187
+ "ListMemoriesSchema",
188
+ "MemoryCreateResponseSchema",
189
+ "MemoryItemResponseSchema",
190
+ "MemoryListResponseSchema",
191
+ "MemoryOperationResponseSchema",
192
+ "MemoryRecord",
193
+ "MemorySearchResponseSchema",
194
+ "MemorySearchResult",
195
+ "MemoryType",
196
+ "RetrievalStrategy",
197
+ "SearchMemorySchema",
198
+ "StoreMemorySchema",
199
+ "UpdateMemorySchema",
200
+ ]
@@ -0,0 +1,4 @@
1
+ from .store_service import StoreService
2
+
3
+
4
+ __all__ = ["StoreService"]
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import HTTPException
6
+ from injectq import inject, singleton
7
+ from tenxgraph.core.state import Message
8
+ from tenxgraph.storage.store import BaseStore
9
+
10
+ from tenxgraph_api.src.app.core import logger
11
+ from tenxgraph_api.src.app.core.auth.request_config import client_config
12
+ from tenxgraph_api.src.app.routers.store.schemas.store_schemas import (
13
+ ForgetMemorySchema,
14
+ MemoryCreateResponseSchema,
15
+ MemoryItemResponseSchema,
16
+ MemoryListResponseSchema,
17
+ MemoryOperationResponseSchema,
18
+ MemorySearchResponseSchema,
19
+ SearchMemorySchema,
20
+ StoreMemorySchema,
21
+ UpdateMemorySchema,
22
+ )
23
+
24
+
25
+ class StoreUnavailableError(HTTPException, ValueError):
26
+ """Raised when the store service has not been configured."""
27
+
28
+ def __init__(self) -> None:
29
+ detail = "Store is not configured"
30
+ HTTPException.__init__(self, status_code=503, detail=detail)
31
+ ValueError.__init__(self, detail)
32
+
33
+
34
+ # Memory fields the store owns. Client metadata is kept, but these keys are dropped from it:
35
+ # user_id and thread_id decide who sees a memory, and memory_id is the id Mem0 reports back.
36
+ SYSTEM_METADATA_KEYS = frozenset(
37
+ {"content", "user_id", "thread_id", "memory_type", "category", "timestamp", "memory_id"}
38
+ )
39
+
40
+ # Store keyword options only the server sets. ``astore`` upserts, so a client-chosen
41
+ # memory_id could replace an existing memory; the store always generates the id instead.
42
+ SERVER_OPTION_KEYS = frozenset({"memory_id"})
43
+
44
+ # Config keys that route storage. A client-chosen collection could read another application's
45
+ # collection or create collections without limit.
46
+ SERVER_CONFIG_KEYS = frozenset({"collection"})
47
+
48
+
49
+ def _client_metadata(metadata: dict[str, Any] | None) -> dict[str, Any] | None:
50
+ if metadata is None:
51
+ return None
52
+ return {k: v for k, v in metadata.items() if k not in SYSTEM_METADATA_KEYS}
53
+
54
+
55
+ def _client_options(options: dict[str, Any] | None) -> dict[str, Any]:
56
+ return {k: v for k, v in (options or {}).items() if k not in SERVER_OPTION_KEYS}
57
+
58
+
59
+ @singleton
60
+ class StoreService:
61
+ """Service layer wrapping interactions with the configured BaseStore."""
62
+
63
+ @inject
64
+ def __init__(self, store: BaseStore | None):
65
+ self.store = store
66
+
67
+ def _get_store(self) -> BaseStore:
68
+ if not self.store:
69
+ raise StoreUnavailableError()
70
+ return self.store
71
+
72
+ def _config(self, config: dict[str, Any] | None, user: dict[str, Any]) -> dict[str, Any]:
73
+ # Client keys pass through; server-owned keys (authz, user, internals) never do.
74
+ # The trusted user object carries the authz policy the store enforces.
75
+ cfg: dict[str, Any] = {
76
+ k: v for k, v in client_config(config).items() if k not in SERVER_CONFIG_KEYS
77
+ }
78
+ cfg["user"] = user
79
+ cfg["user_id"] = user.get("user_id", "anonymous")
80
+ return cfg
81
+
82
+ async def store_memory(
83
+ self,
84
+ payload: StoreMemorySchema,
85
+ user: dict[str, Any],
86
+ ) -> MemoryCreateResponseSchema:
87
+ store = self._get_store()
88
+ cfg = self._config(payload.config, user)
89
+ options = _client_options(payload.options)
90
+
91
+ if isinstance(payload.content, Message):
92
+ content: str | Message = payload.content
93
+ else:
94
+ content = payload.content
95
+
96
+ memory_id = await store.astore(
97
+ cfg,
98
+ content,
99
+ memory_type=payload.memory_type,
100
+ category=payload.category,
101
+ metadata=_client_metadata(payload.metadata),
102
+ **options,
103
+ )
104
+ logger.debug("Stored memory with id %s", memory_id)
105
+ return MemoryCreateResponseSchema(memory_id=memory_id)
106
+
107
+ async def search_memories(
108
+ self,
109
+ payload: SearchMemorySchema,
110
+ user: dict[str, Any],
111
+ ) -> MemorySearchResponseSchema:
112
+ store = self._get_store()
113
+ cfg = self._config(payload.config, user)
114
+ options = _client_options(payload.options)
115
+
116
+ results = await store.asearch(
117
+ cfg,
118
+ payload.query,
119
+ memory_type=payload.memory_type,
120
+ category=payload.category,
121
+ limit=payload.limit,
122
+ score_threshold=payload.score_threshold,
123
+ filters=payload.filters,
124
+ retrieval_strategy=payload.retrieval_strategy,
125
+ distance_metric=payload.distance_metric,
126
+ max_tokens=payload.max_tokens,
127
+ **options,
128
+ )
129
+ return MemorySearchResponseSchema(results=results)
130
+
131
+ async def get_memory(
132
+ self,
133
+ memory_id: str,
134
+ config: dict[str, Any] | None,
135
+ user: dict[str, Any],
136
+ options: dict[str, Any] | None = None,
137
+ ) -> MemoryItemResponseSchema:
138
+ store = self._get_store()
139
+ cfg = self._config(config, user)
140
+ result = await store.aget(cfg, memory_id, **_client_options(options))
141
+ return MemoryItemResponseSchema(memory=result)
142
+
143
+ async def list_memories(
144
+ self,
145
+ config: dict[str, Any] | None,
146
+ user: dict[str, Any],
147
+ limit: int = 100,
148
+ options: dict[str, Any] | None = None,
149
+ ) -> MemoryListResponseSchema:
150
+ store = self._get_store()
151
+ cfg = self._config(config, user)
152
+ memories = await store.aget_all(cfg, limit=limit, **_client_options(options))
153
+ return MemoryListResponseSchema(memories=memories)
154
+
155
+ async def update_memory(
156
+ self,
157
+ memory_id: str,
158
+ payload: UpdateMemorySchema,
159
+ user: dict[str, Any],
160
+ ) -> MemoryOperationResponseSchema:
161
+ store = self._get_store()
162
+ cfg = self._config(payload.config, user)
163
+ options = _client_options(payload.options)
164
+
165
+ result = await store.aupdate(
166
+ cfg,
167
+ memory_id,
168
+ payload.content,
169
+ metadata=_client_metadata(payload.metadata),
170
+ **options,
171
+ )
172
+ return MemoryOperationResponseSchema(success=True, data=result)
173
+
174
+ async def delete_memory(
175
+ self,
176
+ memory_id: str,
177
+ config: dict[str, Any] | None,
178
+ user: dict[str, Any],
179
+ options: dict[str, Any] | None = None,
180
+ ) -> MemoryOperationResponseSchema:
181
+ store = self._get_store()
182
+ cfg = self._config(config, user)
183
+ result = await store.adelete(cfg, memory_id, **_client_options(options))
184
+ return MemoryOperationResponseSchema(success=True, data=result)
185
+
186
+ async def forget_memory(
187
+ self,
188
+ payload: ForgetMemorySchema,
189
+ user: dict[str, Any],
190
+ ) -> MemoryOperationResponseSchema:
191
+ store = self._get_store()
192
+ cfg = self._config(payload.config, user)
193
+ options = _client_options(payload.options)
194
+ forget_kwargs: dict[str, Any] = {
195
+ "memory_type": payload.memory_type,
196
+ "category": payload.category,
197
+ "filters": payload.filters,
198
+ }
199
+ # Remove None values before forwarding to the store
200
+ forget_kwargs = {k: v for k, v in forget_kwargs.items() if v is not None}
201
+ forget_kwargs.update(options)
202
+ result = await store.aforget_memory(cfg, **forget_kwargs)
203
+ return MemoryOperationResponseSchema(success=True, data=result)
File without changes
@@ -0,0 +1,38 @@
1
+ # from uuid import UUID
2
+
3
+ # from fastapi_injector import InjectedTaskiq
4
+
5
+ # from src.app.routers.auth.services import UserService
6
+ # from src.app.worker import broker
7
+
8
+
9
+ # @broker.task(task_name="add_task_math")
10
+ # def add_task_math(x: int, y: int):
11
+ # """
12
+ # Adds two integers together.
13
+
14
+ # Args:
15
+ # x (int): The first integer.
16
+ # y (int): The second integer.
17
+
18
+ # Returns:
19
+ # int: The sum of the two integers.
20
+ # """
21
+ # return x + y
22
+
23
+
24
+ # @broker.task(task_name="user_service_post_processing")
25
+ # async def post_processing_user(user_id: int, service: UserService = InjectedTaskiq(UserService)):
26
+ # """
27
+ # Post-processes a user by fetching user details using the provided user service.
28
+
29
+ # Args:
30
+ # user_id (int): The ID of the user to be processed.
31
+ # service (UserService, optional): An instance of UserService to fetch user details.
32
+ # Defaults to an injected instance of UserService.
33
+
34
+ # Returns:
35
+ # dict: A dictionary representation of the user details.
36
+ # """
37
+ # user = await service.get_user(UUID("2b1e2ae4-ff8a-48f3-8729-be0d754432e9"))
38
+ # return user.dict()
@@ -0,0 +1,12 @@
1
+ from .response_helper import error_response, success_response
2
+ from .swagger_helper import generate_swagger_responses
3
+ from .thread_name_generator import DefaultThreadNameGenerator, ThreadNameGenerator
4
+
5
+
6
+ __all__ = [
7
+ "DefaultThreadNameGenerator",
8
+ "ThreadNameGenerator",
9
+ "error_response",
10
+ "generate_swagger_responses",
11
+ "success_response",
12
+ ]
@@ -0,0 +1,26 @@
1
+ import asyncio
2
+ import inspect
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+
7
+ def _is_async_callable(func: Callable[..., Any]) -> bool:
8
+ """Detect if a callable is a coroutine function (async def)."""
9
+ return asyncio.iscoroutinefunction(func)
10
+
11
+
12
+ async def call_sync_or_async(func: Callable[..., Any], *args, **kwargs) -> Any:
13
+ """
14
+ Call a function that may be sync or async, returning its result.
15
+ If sync, runs in a thread pool to avoid blocking the event loop.
16
+ Thread pool is shared for efficiency.
17
+ """
18
+ if _is_async_callable(func):
19
+ return await func(*args, **kwargs)
20
+
21
+ # Call sync function in a thread pool
22
+ result = await asyncio.to_thread(func, *args, **kwargs)
23
+ # If the result is awaitable, await it
24
+ if inspect.isawaitable(result):
25
+ return await result
26
+ return result
@@ -0,0 +1,15 @@
1
+ """Media processing module for 10xgraph-api.
2
+
3
+ Document extraction lives here (not in 10xGraph core) because
4
+ the core library stays lightweight — SDK users extract text themselves.
5
+ The API platform auto-extracts using textxtract.
6
+ """
7
+
8
+ from .extractor import DocumentExtractor
9
+ from .pipeline import DocumentPipeline
10
+
11
+
12
+ __all__ = [
13
+ "DocumentExtractor",
14
+ "DocumentPipeline",
15
+ ]
@@ -0,0 +1,172 @@
1
+ """Document extraction service using textxtract.
2
+
3
+ Wraps ``AsyncTextExtractor`` from the textxtract library.
4
+ This module lives in 10xgraph-api, NOT in the core 10xGraph library,
5
+ because extraction is an API-platform concern.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import io
12
+ import logging
13
+ import zipfile
14
+ from pathlib import PurePosixPath
15
+ from typing import Any
16
+
17
+
18
+ logger = logging.getLogger("tenxgraph_api.media.extractor")
19
+
20
+ try:
21
+ from textxtract import AsyncTextExtractor
22
+ from textxtract.core.exceptions import ExtractionError, FileTypeNotSupportedError
23
+ except ImportError: # pragma: no cover
24
+ AsyncTextExtractor = None # type: ignore[assignment]
25
+
26
+
27
+ # The handler textxtract runs is chosen from the file extension. Uploads never pass their
28
+ # own file name through: the declared MIME type picks one of these extensions, and a file
29
+ # with any other type (archives included) is not extracted. That keeps a file named
30
+ # ``a.zip`` out of textxtract's ZIP handler, which unpacks nested archives with no overall
31
+ # size budget.
32
+ HANDLER_EXTENSIONS: dict[str, str] = {
33
+ "application/pdf": ".pdf",
34
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
35
+ "application/msword": ".doc",
36
+ "text/html": ".html",
37
+ "text/xml": ".xml",
38
+ "application/xml": ".xml",
39
+ "text/markdown": ".md",
40
+ "text/csv": ".csv",
41
+ "application/json": ".json",
42
+ "text/plain": ".txt",
43
+ }
44
+ _EXTRACTABLE_EXTENSIONS = frozenset(HANDLER_EXTENSIONS.values())
45
+
46
+ # DOCX files are ZIP containers, so they are inspected before parsing: a small upload can
47
+ # declare gigabytes of XML.
48
+ MAX_ARCHIVE_UNCOMPRESSED_BYTES = 50 * 1024 * 1024
49
+ MAX_COMPRESSION_RATIO = 100
50
+ MAX_ARCHIVE_ENTRIES = 10_000
51
+
52
+ EXTRACTION_TIMEOUT_SECONDS = 30.0
53
+ MAX_EXTRACTED_CHARS = 1_000_000
54
+
55
+
56
+ def _handler_extension(filename: str | None, mime_type: str | None) -> str | None:
57
+ """The extension (and so the handler) to extract with, or None to skip extraction."""
58
+ if mime_type:
59
+ return HANDLER_EXTENSIONS.get(mime_type.split(";", 1)[0].strip().lower())
60
+ suffix = PurePosixPath(filename or "").suffix.lower()
61
+ return suffix if suffix in _EXTRACTABLE_EXTENSIONS else None
62
+
63
+
64
+ def _check_zip_container(data: bytes) -> None:
65
+ """Reject a ZIP-based document whose contents would expand far beyond its size."""
66
+ try:
67
+ with zipfile.ZipFile(io.BytesIO(data)) as archive:
68
+ entries = archive.infolist()
69
+ except zipfile.BadZipFile as exc:
70
+ raise ValueError("Document is not a valid ZIP-based file") from exc
71
+
72
+ if len(entries) > MAX_ARCHIVE_ENTRIES:
73
+ raise ValueError("Document has too many parts to extract")
74
+ total = sum(entry.file_size for entry in entries)
75
+ if total > MAX_ARCHIVE_UNCOMPRESSED_BYTES:
76
+ raise ValueError("Document is too large to extract once uncompressed")
77
+ for entry in entries:
78
+ if entry.file_size > MAX_COMPRESSION_RATIO * max(entry.compress_size, 1):
79
+ raise ValueError(f"Document part {entry.filename!r} has a suspicious compression ratio")
80
+
81
+
82
+ def _as_text(result: Any) -> str | None:
83
+ if result is None:
84
+ return None
85
+ if isinstance(result, list | tuple):
86
+ result = "\n\n".join(str(part) for part in result)
87
+ text = str(result)
88
+ if len(text) > MAX_EXTRACTED_CHARS:
89
+ logger.warning(
90
+ "Extracted text truncated from %d to %d chars", len(text), MAX_EXTRACTED_CHARS
91
+ )
92
+ text = text[:MAX_EXTRACTED_CHARS]
93
+ return text
94
+
95
+
96
+ class DocumentExtractor:
97
+ """Wraps textxtract AsyncTextExtractor for API-side document extraction.
98
+
99
+ Examples::
100
+
101
+ extractor = DocumentExtractor()
102
+ text = await extractor.extract(pdf_bytes, "report.pdf")
103
+ """
104
+
105
+ def __init__(self, extractor: Any | None = None):
106
+ if extractor is not None:
107
+ self.extractor = extractor
108
+ return
109
+
110
+ if AsyncTextExtractor is None:
111
+ raise ImportError(
112
+ "textxtract is required for document extraction. "
113
+ 'Install with `pip install "10xgraph-api[media]"`'
114
+ )
115
+
116
+ self.extractor = AsyncTextExtractor()
117
+
118
+ async def extract(
119
+ self,
120
+ data: bytes | str,
121
+ filename: str | None = None,
122
+ mime_type: str | None = None,
123
+ ) -> str | None:
124
+ """Extract text from bytes or a local path.
125
+
126
+ For bytes, the handler comes from ``mime_type`` when given, otherwise from the
127
+ extension of ``filename``, and only document types are extracted: archives and
128
+ unknown types return ``None``. The client's file name is never handed to textxtract.
129
+
130
+ Args:
131
+ data: Raw bytes or local path.
132
+ filename: Required when passing bytes (used only when ``mime_type`` is absent).
133
+ mime_type: The declared content type; preferred for picking the handler.
134
+
135
+ Returns:
136
+ Extracted text (capped at ``MAX_EXTRACTED_CHARS``), or ``None`` when the file
137
+ type is not extracted.
138
+
139
+ Raises:
140
+ ValueError: For a missing filename, an oversized or bomb-like document, a
141
+ timeout, or failed extraction.
142
+ """
143
+ if isinstance(data, bytes) and not filename:
144
+ raise ValueError("filename must be provided when extracting from bytes")
145
+
146
+ try:
147
+ if isinstance(data, bytes):
148
+ extension = _handler_extension(filename, mime_type)
149
+ if extension is None:
150
+ logger.info(
151
+ "Not extracting %s (%s): not a supported document", filename, mime_type
152
+ )
153
+ return None
154
+ if extension == ".docx":
155
+ _check_zip_container(data)
156
+ result = await asyncio.wait_for(
157
+ self.extractor.extract(data, f"upload{extension}"),
158
+ timeout=EXTRACTION_TIMEOUT_SECONDS,
159
+ )
160
+ return _as_text(result)
161
+ return _as_text(await self.extractor.extract(data))
162
+
163
+ except TimeoutError as exc:
164
+ logger.warning("Document extraction timed out for %s", filename)
165
+ raise ValueError("Document extraction timed out") from exc
166
+
167
+ except FileTypeNotSupportedError: # type: ignore
168
+ logger.warning("Document type not supported for extraction: %s", filename)
169
+ return None
170
+ except ExtractionError as exc: # type: ignore
171
+ logger.exception("Document extraction failed for %s", filename)
172
+ raise ValueError("Failed to extract text from document") from exc