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,83 @@
1
+ """Document processing pipeline for the API layer.
2
+
3
+ Orchestrates: receive uploaded file -> extract text via DocumentExtractor
4
+ -> return TextBlock or DocumentBlock depending on config.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from base64 import b64decode
11
+ from typing import Any
12
+
13
+ from tenxgraph.core.state.message_block import DocumentBlock, TextBlock
14
+ from tenxgraph.storage.media.config import DocumentHandling
15
+
16
+ from .extractor import DocumentExtractor
17
+
18
+
19
+ logger = logging.getLogger("tenxgraph_api.media.pipeline")
20
+
21
+
22
+ class DocumentPipeline:
23
+ """Document processing pipeline with extraction config.
24
+
25
+ Modes:
26
+ - ``EXTRACT_TEXT``: Extract text via textxtract, return TextBlock.
27
+ - ``PASS_RAW``/``FORWARD_RAW``: Return the original DocumentBlock untouched.
28
+ - ``SKIP``: Drop the document entirely (returns None).
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ document_extractor: DocumentExtractor | None = None,
34
+ handling: DocumentHandling = DocumentHandling.EXTRACT_TEXT,
35
+ ):
36
+ self.handling = handling
37
+ # Lazy-init extractor only when actually needed (EXTRACT_TEXT mode)
38
+ self._extractor = document_extractor
39
+
40
+ @property
41
+ def extractor(self) -> DocumentExtractor:
42
+ if self._extractor is None:
43
+ self._extractor = DocumentExtractor()
44
+ return self._extractor
45
+
46
+ async def process_document(self, document_block: Any) -> Any | None:
47
+ """Process a DocumentBlock according to the handling policy.
48
+
49
+ Args:
50
+ document_block: A ``DocumentBlock`` instance.
51
+
52
+ Returns:
53
+ A ``TextBlock`` (if extracted), the original ``DocumentBlock``
54
+ (if pass-raw or extraction unsupported), or ``None`` (if skip).
55
+ """
56
+ if self.handling == DocumentHandling.SKIP:
57
+ return None
58
+
59
+ if self.handling == DocumentHandling.FORWARD_RAW:
60
+ return document_block
61
+
62
+ # EXTRACT_TEXT path
63
+ if not isinstance(document_block, DocumentBlock):
64
+ raise ValueError("Expected DocumentBlock in pipeline")
65
+
66
+ # If an excerpt already exists, return it as text
67
+ if document_block.excerpt and document_block.excerpt.strip():
68
+ return TextBlock(text=document_block.excerpt)
69
+
70
+ media = document_block.media
71
+
72
+ if media.kind == "data" and media.data_base64:
73
+ decoded = b64decode(media.data_base64)
74
+ filename = media.filename or "document.pdf"
75
+ extracted = await self.extractor.extract(decoded, filename, media.mime_type)
76
+ if extracted:
77
+ return TextBlock(text=extracted)
78
+ # Extraction returned None (unsupported type) — keep raw
79
+ return document_block
80
+
81
+ # For external URLs or provider file_id, we cannot extract directly.
82
+ # Keep as-is so API layer may resolve later.
83
+ return document_block
@@ -0,0 +1,17 @@
1
+ from typing import Any
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from tenxgraph_api.src.app.core.config.settings import Settings
6
+
7
+
8
+ def parse_state_output(settings: Settings, response: BaseModel) -> dict[str, Any]:
9
+ # if settings.IS_DEBUG:
10
+ # return response.model_dump(exclude={"execution_meta"}, serialize_as_any=True)
11
+ return response.model_dump(serialize_as_any=True)
12
+
13
+
14
+ def parse_message_output(settings: Settings, response: BaseModel) -> dict[str, Any]:
15
+ # if settings.IS_DEBUG:
16
+ # return response.model_dump(exclude={"raw"}, serialize_as_any=True)
17
+ return response.model_dump(serialize_as_any=True)
@@ -0,0 +1,129 @@
1
+ """This module contains helper functions for creating responses
2
+ with metadata, error codes, and messages. The functions in this
3
+ module are used to create responses with metadata, error codes,
4
+ and messages. The success_response function is used to create
5
+ a success response with the provided data, message, status code,
6
+ and metadata. The error_response function is used to create an
7
+ error response with the provided error code, message, details,
8
+ status code, and metadata. The merge_metadata function is used
9
+ to merge metadata with request details and a message.
10
+
11
+ Examples:
12
+ - success_response(res, request, message, status_code, metadata)
13
+ - error_response(request, error_code, message, details, status_code, metadata)
14
+ - merge_metadata(metadata, request, message)
15
+
16
+ """
17
+
18
+ from typing import Any, TypeVar
19
+
20
+ from fastapi import Request
21
+ from fastapi.responses import ORJSONResponse
22
+
23
+ from .schemas import (
24
+ ErrorOutputSchema,
25
+ ErrorResponse,
26
+ ErrorSchemas,
27
+ SuccessResponse,
28
+ )
29
+
30
+
31
+ T = TypeVar("T")
32
+
33
+
34
+ def merge_metadata(metadata: dict | None, request: Request, message: str = "") -> dict[Any, Any]:
35
+ """Merges metadata with request details and a message.
36
+ The metadata is updated with the request_id, timestamp,
37
+ and message. This function is used to create a response
38
+ with metadata, request details, and a message.
39
+
40
+ Args:
41
+ metadata: A dictionary containing metadata information. Defaults to None.
42
+ request: An instance of the Request class from fastapi. Defaults to None.
43
+ message: A string message to be included in the metadata. Defaults to "".
44
+
45
+ Returns:
46
+ metadata: A dictionary containing merged metadata with request details
47
+ and the message.
48
+ """
49
+ if metadata:
50
+ metadata.update(
51
+ {
52
+ "request_id": request.state.request_id,
53
+ "timestamp": request.state.timestamp,
54
+ "message": message,
55
+ }
56
+ )
57
+ return metadata
58
+
59
+ return {
60
+ "request_id": request.state.request_id,
61
+ "timestamp": request.state.timestamp,
62
+ "message": message,
63
+ }
64
+
65
+
66
+ def success_response(
67
+ res: Any,
68
+ request: Request,
69
+ message: str = "OK",
70
+ status_code: int = 200,
71
+ metadata: dict | None = None,
72
+ ):
73
+ """Creates a success response with the provided data, message,
74
+ status code, and metadata.
75
+
76
+ Args:
77
+ res (T): The data to be included in the response.
78
+ request (Request): The FastAPI request object.
79
+ message (str, optional): The message associated with the response.
80
+ Defaults to OK.
81
+ status_code (int, optional): The HTTP status code of the response.
82
+ Defaults to 200.
83
+ metadata (dict, optional): Additional metadata to be merged with the response.
84
+ Defaults to None.
85
+
86
+ Returns:
87
+ ORJSONResponse: The JSON response containing the data,
88
+ message, and metadata.
89
+ """
90
+ response: SuccessResponse = SuccessResponse(
91
+ data=res, metadata=merge_metadata(metadata, request, message)
92
+ )
93
+ return ORJSONResponse(response.model_dump(), status_code=status_code)
94
+
95
+
96
+ def error_response(
97
+ request: Request,
98
+ error_code: str,
99
+ message: str = "",
100
+ details: list[ErrorSchemas] | None = None,
101
+ status_code: int = 400,
102
+ metadata: dict | None = None,
103
+ ):
104
+ """Creates an error response with the provided error code, message,
105
+ details, status code, and metadata.
106
+
107
+ Args:
108
+ request (Request): The FastAPI request object. Default to None.
109
+ error_code (str): The code associated with the error.
110
+ Default to None.
111
+ message (str, optional): The message describing the error. Default to "".
112
+ details (list[ErrorSchemas], optional): Additional details about the error.
113
+ Default to None.
114
+ status_code (int, optional): The HTTP status code of the response.
115
+ Default is 400.
116
+ metadata (dict, optional): Additional metadata to be merged with the response.
117
+ Default is None.
118
+
119
+ Returns:
120
+ ORJSONResponse: The JSON response containing the error details, message,
121
+ and metadata.
122
+ """
123
+ error = ErrorResponse(
124
+ error=ErrorOutputSchema(
125
+ code=error_code, message=message, details=details if details else []
126
+ ),
127
+ metadata=merge_metadata(metadata, request, message),
128
+ )
129
+ return ORJSONResponse(error.model_dump(), status_code=status_code)
@@ -0,0 +1,11 @@
1
+ from .output_schemas import ErrorOutputSchema, ErrorResponse, ErrorSchemas, SuccessResponse
2
+ from .user_schemas import AuthUserSchema
3
+
4
+
5
+ __all__ = [
6
+ "AuthUserSchema",
7
+ "ErrorOutputSchema",
8
+ "ErrorResponse",
9
+ "ErrorSchemas",
10
+ "SuccessResponse",
11
+ ]
@@ -0,0 +1,28 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ class ErrorSchemas(BaseModel):
10
+ loc: list[str | int] = Field(..., title="Location")
11
+ msg: str = Field(..., title="Error message")
12
+ type: str = Field(..., title="Error type")
13
+
14
+
15
+ class ErrorOutputSchema(BaseModel):
16
+ code: str = Field(..., title="Error code")
17
+ message: str = Field(..., title="Error message")
18
+ details: list[ErrorSchemas] = Field([], title="Error details")
19
+
20
+
21
+ class ErrorResponse(BaseModel):
22
+ error: ErrorOutputSchema | None = None
23
+ metadata: dict = Field({}, title="Metadata")
24
+
25
+
26
+ class SuccessResponse(BaseModel, Generic[T]):
27
+ data: T | None = None
28
+ metadata: dict = Field({}, title="Metadata")
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class AuthUserSchema(BaseModel):
5
+ name: str
6
+ role: str
7
+ company: int
8
+ uuid: str
9
+ user_id: str
10
+ email: str
11
+ email_verified: bool
12
+ firebase: dict
13
+ uid: str
@@ -0,0 +1,90 @@
1
+ import os
2
+ from importlib.util import find_spec
3
+
4
+ from tenxgraph.utils.id_generator import BaseIDGenerator, IDType
5
+
6
+
7
+ # Check if snowflakekit is available
8
+ HAS_SNOWFLAKE = find_spec("snowflakekit") is not None
9
+
10
+
11
+ class SnowFlakeIdGenerator(BaseIDGenerator):
12
+ def __init__(
13
+ self,
14
+ snowflake_epoch: int | None = None,
15
+ total_bits: int | None = None,
16
+ snowflake_time_bits: int | None = None,
17
+ snowflake_node_bits: int | None = None,
18
+ snowflake_node_id: int | None = None,
19
+ snowflake_worker_id: int | None = None,
20
+ snowflake_worker_bits: int | None = None,
21
+ ):
22
+ # IF all these are None then try to read from env
23
+ config = None
24
+ if not HAS_SNOWFLAKE:
25
+ raise ImportError(
26
+ "snowflakekit is not installed. Please install it to use SnowFlakeIdGenerator."
27
+ )
28
+
29
+ from snowflakekit import SnowflakeConfig, SnowflakeGenerator
30
+
31
+ if (
32
+ snowflake_epoch is None
33
+ and total_bits is None
34
+ and snowflake_time_bits is None
35
+ and snowflake_node_bits is None
36
+ and snowflake_node_id is None
37
+ and snowflake_worker_id is None
38
+ and snowflake_worker_bits is None
39
+ ):
40
+ snowflake_epoch = int(os.environ.get("SNOWFLAKE_EPOCH", "1723323246031"))
41
+ total_bits = int(os.environ.get("SNOWFLAKE_TOTAL_BITS", "64"))
42
+ snowflake_time_bits = int(os.environ.get("SNOWFLAKE_TIME_BITS", "39"))
43
+ snowflake_node_bits = int(os.environ.get("SNOWFLAKE_NODE_BITS", "7"))
44
+ snowflake_node_id = int(os.environ.get("SNOWFLAKE_NODE_ID", "0"))
45
+ snowflake_worker_id = int(os.environ.get("SNOWFLAKE_WORKER_ID", "0"))
46
+ snowflake_worker_bits = int(os.environ.get("SNOWFLAKE_WORKER_BITS", "5"))
47
+
48
+ config = SnowflakeConfig(
49
+ epoch=snowflake_epoch,
50
+ total_bits=total_bits,
51
+ time_bits=snowflake_time_bits,
52
+ node_bits=snowflake_node_bits,
53
+ node_id=snowflake_node_id,
54
+ worker_id=snowflake_worker_id,
55
+ worker_bits=snowflake_worker_bits,
56
+ )
57
+
58
+ elif (
59
+ snowflake_epoch is not None
60
+ and total_bits is not None
61
+ and snowflake_time_bits is not None
62
+ and snowflake_node_bits is not None
63
+ and snowflake_node_id is not None
64
+ and snowflake_worker_id is not None
65
+ and snowflake_worker_bits is not None
66
+ ):
67
+ config = SnowflakeConfig(
68
+ epoch=snowflake_epoch,
69
+ total_bits=total_bits,
70
+ time_bits=snowflake_time_bits,
71
+ node_bits=snowflake_node_bits,
72
+ node_id=snowflake_node_id,
73
+ worker_id=snowflake_worker_id,
74
+ worker_bits=snowflake_worker_bits,
75
+ )
76
+
77
+ # Now create generator
78
+ self.generator = SnowflakeGenerator(config=config)
79
+
80
+ @property
81
+ def id_type(self) -> IDType:
82
+ return IDType.BIGINT
83
+
84
+ async def generate(self) -> int:
85
+ """Generate a new Snowflake ID.
86
+
87
+ Returns:
88
+ int: A new Snowflake ID.
89
+ """
90
+ return await self.generator.generate()
@@ -0,0 +1,160 @@
1
+ """
2
+ Swagger Helper Module
3
+ Contains helper functions to generate Swagger Responses
4
+ """
5
+
6
+ import uuid
7
+ from datetime import datetime
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from .schemas import (
13
+ ErrorOutputSchema,
14
+ ErrorSchemas,
15
+ )
16
+
17
+
18
+ class _SwaggerSuccessSchemas[T](BaseModel):
19
+ """
20
+ Swagger Success Response Schema
21
+ """
22
+
23
+ data: T
24
+ metadata: dict = {
25
+ "message": "Success",
26
+ "request_id": uuid.uuid4().hex,
27
+ "timestamp": datetime.now().isoformat(),
28
+ }
29
+
30
+
31
+ class _SwaggerSuccessPaginationSchemas[T](BaseModel):
32
+ data: T
33
+ metadata: dict = {
34
+ "message": "Success",
35
+ "request_id": uuid.uuid4().hex,
36
+ "timestamp": datetime.now().isoformat(),
37
+ "pagination": {
38
+ "total": 100,
39
+ "offset": 0,
40
+ "limit": 10,
41
+ },
42
+ }
43
+
44
+
45
+ class _SwaggerError400Schemas(BaseModel):
46
+ metadata: dict = {
47
+ "message": "Failed",
48
+ "request_id": uuid.uuid4().hex,
49
+ "timestamp": datetime.now().isoformat(),
50
+ }
51
+ error: ErrorOutputSchema = ErrorOutputSchema(
52
+ code="BAD_REQUEST",
53
+ message="Invalid input, please check the input data for any errors",
54
+ details=[],
55
+ )
56
+
57
+
58
+ class _SwaggerError404Schemas(BaseModel):
59
+ metadata: dict = {
60
+ "message": "Failed",
61
+ "request_id": uuid.uuid4().hex,
62
+ "timestamp": datetime.now().isoformat(),
63
+ }
64
+ error: ErrorOutputSchema = ErrorOutputSchema(
65
+ code="RESOURCE_NOT_FOUND", message="Resource not found", details=[]
66
+ )
67
+
68
+
69
+ class _SwaggerError401Schemas(BaseModel):
70
+ metadata: dict = {
71
+ "message": "Failed",
72
+ "request_id": uuid.uuid4().hex,
73
+ "timestamp": datetime.now().isoformat(),
74
+ }
75
+ error: ErrorOutputSchema = ErrorOutputSchema(
76
+ code="AUTHENTICATION_FAILED",
77
+ message="Please provide valid credentials",
78
+ details=[],
79
+ )
80
+
81
+
82
+ class _SwaggerError403Schemas(BaseModel):
83
+ metadata: dict = {
84
+ "message": "Failed",
85
+ "request_id": uuid.uuid4().hex,
86
+ "timestamp": datetime.now().isoformat(),
87
+ }
88
+ error: ErrorOutputSchema = ErrorOutputSchema(
89
+ code="PERMISSION_ERROR",
90
+ message="You don't have permission to access this resource",
91
+ details=[],
92
+ )
93
+
94
+
95
+ class _SwaggerError426Schemas(BaseModel):
96
+ metadata: dict = {
97
+ "message": "Failed",
98
+ "request_id": uuid.uuid4().hex,
99
+ "timestamp": datetime.now().isoformat(),
100
+ }
101
+ error: ErrorOutputSchema = ErrorOutputSchema(
102
+ code="VALIDATION_ERROR",
103
+ message="Invalid input",
104
+ details=[
105
+ ErrorSchemas(
106
+ loc=["body", "name"],
107
+ msg="field required",
108
+ type="value_error.missing",
109
+ )
110
+ ],
111
+ )
112
+
113
+
114
+ def generate_swagger_responses(
115
+ model: type[BaseModel],
116
+ show_pagination: bool = False,
117
+ ) -> dict[int | str, dict[str, Any]]:
118
+ """
119
+ Generate Swagger Responses Example, based on the provided model.
120
+ This will generate the following responses:
121
+ - 400: Invalid input
122
+ - 404: Resource not found
123
+ - 401: Authentication failed
124
+ - 403: Forbidden
125
+ - 422: Validation error
126
+ - 200: Success
127
+
128
+ The 200 response will be based on the provided model. If show_pagination is True,
129
+ then the response will have pagination metadata.
130
+ It will be used to generate redoc and swagger documentation.
131
+
132
+ Args:
133
+ model (type[BaseModel]): Base Pydantic Model class, which will be used as response.
134
+ show_pagination (bool, optional): Show Pagination in Swagger Response. Defaults to False.
135
+
136
+ Returns:
137
+ dict[int, dict[str, Any]]: Collection of Swagger Responses
138
+
139
+ """
140
+ return {
141
+ 400: {"model": _SwaggerError400Schemas, "description": "Invalid input"},
142
+ 404: {
143
+ "model": _SwaggerError404Schemas,
144
+ "description": "Resource not found",
145
+ },
146
+ 401: {
147
+ "model": _SwaggerError401Schemas,
148
+ "description": "Authentication failed",
149
+ },
150
+ 403: {"model": _SwaggerError403Schemas, "description": "Forbidden"},
151
+ 422: {
152
+ "model": _SwaggerError426Schemas,
153
+ "description": "Validation error",
154
+ },
155
+ 200: {
156
+ "model": _SwaggerSuccessPaginationSchemas[model] # type: ignore
157
+ if show_pagination
158
+ else _SwaggerSuccessSchemas[model], # type: ignore
159
+ },
160
+ }
@@ -0,0 +1,123 @@
1
+ """In-memory telemetry store for observability.
2
+
3
+ Captures per-run streaming records (one per ``StreamChunk``) keyed by thread, so
4
+ the observability endpoint can reconstruct a trace (spans + events + token cost)
5
+ from the events a run actually emitted. This is process-local and best-effort —
6
+ it is not a durable telemetry backend; restart clears it.
7
+
8
+ A single instance is bound as an InjectQ singleton and written to by the graph
9
+ service during invoke/stream runs.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import threading
15
+ from collections import deque
16
+ from typing import Any
17
+
18
+
19
+ # Bounds so long-lived servers don't grow unbounded.
20
+ MAX_THREADS = 200
21
+ MAX_RUNS_PER_THREAD = 20
22
+ MAX_RECORDS_PER_RUN = 2000
23
+
24
+
25
+ class RunTrace:
26
+ """A single graph run's captured records."""
27
+
28
+ def __init__(self, run_id: str, thread_id: str, started_at: float) -> None:
29
+ self.run_id = run_id
30
+ self.thread_id = thread_id
31
+ self.started_at = started_at
32
+ self.finished_at: float | None = None
33
+ self.status: str = "running" # running | done | error | stopped
34
+ self.records: deque[dict[str, Any]] = deque(maxlen=MAX_RECORDS_PER_RUN)
35
+
36
+ def add(self, record: dict[str, Any]) -> None:
37
+ self.records.append(record)
38
+
39
+
40
+ class TelemetryStore:
41
+ """Thread-safe, in-memory store of run traces keyed by thread id."""
42
+
43
+ def __init__(self) -> None:
44
+ self._lock = threading.Lock()
45
+ # thread_id -> ordered dict-like: run_id -> RunTrace (kept insertion order)
46
+ self._threads: dict[str, dict[str, RunTrace]] = {}
47
+ self._thread_order: deque[str] = deque(maxlen=MAX_THREADS)
48
+
49
+ def start_run(self, thread_id: str, run_id: str, started_at: float) -> RunTrace:
50
+ thread_id = str(thread_id)
51
+ run_id = str(run_id)
52
+ with self._lock:
53
+ runs = self._threads.get(thread_id)
54
+ if runs is None:
55
+ runs = {}
56
+ self._threads[thread_id] = runs
57
+ if thread_id in self._thread_order:
58
+ self._thread_order.remove(thread_id)
59
+ self._thread_order.append(thread_id)
60
+ # Evict oldest threads beyond the cap.
61
+ while len(self._threads) > MAX_THREADS and self._thread_order:
62
+ oldest = self._thread_order.popleft()
63
+ self._threads.pop(oldest, None)
64
+
65
+ trace = RunTrace(run_id, thread_id, started_at)
66
+ runs[run_id] = trace
67
+ # Cap runs per thread (drop oldest by insertion order).
68
+ while len(runs) > MAX_RUNS_PER_THREAD:
69
+ oldest_run = next(iter(runs))
70
+ runs.pop(oldest_run, None)
71
+ return trace
72
+
73
+ def record(self, thread_id: str, run_id: str, record: dict[str, Any]) -> None:
74
+ thread_id = str(thread_id)
75
+ run_id = str(run_id)
76
+ with self._lock:
77
+ runs = self._threads.get(thread_id)
78
+ if not runs:
79
+ return
80
+ trace = runs.get(run_id)
81
+ if trace:
82
+ trace.add(record)
83
+
84
+ def finish_run(
85
+ self,
86
+ thread_id: str,
87
+ run_id: str,
88
+ finished_at: float,
89
+ status: str = "done",
90
+ ) -> None:
91
+ thread_id = str(thread_id)
92
+ run_id = str(run_id)
93
+ with self._lock:
94
+ runs = self._threads.get(thread_id)
95
+ if not runs:
96
+ return
97
+ trace = runs.get(run_id)
98
+ if trace:
99
+ trace.finished_at = finished_at
100
+ trace.status = status
101
+
102
+ def get_runs(self, thread_id: str) -> list[RunTrace]:
103
+ """Runs for a thread, newest last (insertion order)."""
104
+ with self._lock:
105
+ runs = self._threads.get(str(thread_id))
106
+ return list(runs.values()) if runs else []
107
+
108
+ def get_latest_run(self, thread_id: str) -> RunTrace | None:
109
+ runs = self.get_runs(thread_id)
110
+ return runs[-1] if runs else None
111
+
112
+ def get_run(self, thread_id: str, run_id: str) -> RunTrace | None:
113
+ with self._lock:
114
+ runs = self._threads.get(str(thread_id))
115
+ return runs.get(str(run_id)) if runs else None
116
+
117
+ def delete_thread(self, thread_id: str) -> None:
118
+ """Completely clear a thread's traces from the in-memory telemetry cache."""
119
+ thread_id = str(thread_id)
120
+ with self._lock:
121
+ self._threads.pop(thread_id, None)
122
+ if thread_id in self._thread_order:
123
+ self._thread_order.remove(thread_id)