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.
- 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
- 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
- 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
- 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
- 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
- 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
- agentflow_cli/__init__.py +69 -0
- tenxgraph_api/__init__.py +23 -0
- tenxgraph_api/cli/__init__.py +6 -0
- tenxgraph_api/cli/capabilities.py +127 -0
- tenxgraph_api/cli/commands/__init__.py +49 -0
- tenxgraph_api/cli/commands/api.py +262 -0
- tenxgraph_api/cli/commands/audit.py +213 -0
- tenxgraph_api/cli/commands/build.py +305 -0
- tenxgraph_api/cli/commands/config.py +71 -0
- tenxgraph_api/cli/commands/demo.py +93 -0
- tenxgraph_api/cli/commands/eval.py +909 -0
- tenxgraph_api/cli/commands/init.py +531 -0
- tenxgraph_api/cli/commands/skills.py +532 -0
- tenxgraph_api/cli/commands/test.py +113 -0
- tenxgraph_api/cli/commands/version.py +47 -0
- tenxgraph_api/cli/config_editor/__init__.py +15 -0
- tenxgraph_api/cli/config_editor/schema.py +566 -0
- tenxgraph_api/cli/config_editor/server.py +240 -0
- tenxgraph_api/cli/config_editor/static/app.css +2 -0
- tenxgraph_api/cli/config_editor/static/app.js +3 -0
- tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
- tenxgraph_api/cli/config_editor/static/index.html +20 -0
- tenxgraph_api/cli/config_editor/store.py +120 -0
- tenxgraph_api/cli/config_editor/validation.py +377 -0
- tenxgraph_api/cli/constants.py +112 -0
- tenxgraph_api/cli/context.py +46 -0
- tenxgraph_api/cli/core/__init__.py +0 -0
- tenxgraph_api/cli/core/animations.py +550 -0
- tenxgraph_api/cli/core/config.py +279 -0
- tenxgraph_api/cli/core/output.py +625 -0
- tenxgraph_api/cli/core/prompts.py +220 -0
- tenxgraph_api/cli/core/screen.py +287 -0
- tenxgraph_api/cli/core/steps.py +621 -0
- tenxgraph_api/cli/core/theme.py +242 -0
- tenxgraph_api/cli/core/validation.py +251 -0
- tenxgraph_api/cli/exceptions.py +129 -0
- tenxgraph_api/cli/logger.py +84 -0
- tenxgraph_api/cli/main.py +936 -0
- tenxgraph_api/cli/templates/__init__.py +0 -0
- tenxgraph_api/cli/templates/defaults.py +844 -0
- tenxgraph_api/cli/templates/dev/.env.example +1 -0
- tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
- tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
- tenxgraph_api/cli/templates/prod/.env.example +75 -0
- tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
- tenxgraph_api/cli/templates/prod/.python-version +1 -0
- tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
- tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
- tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
- tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
- tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
- tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
- tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
- tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
- tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
- tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
- tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
- tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
- tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
- tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
- tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
- tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
- tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
- tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
- tenxgraph_api/py.typed +0 -0
- tenxgraph_api/src/__init__.py +0 -0
- tenxgraph_api/src/app/__init__.py +13 -0
- tenxgraph_api/src/app/core/__init__.py +15 -0
- tenxgraph_api/src/app/core/auth/__init__.py +0 -0
- tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
- tenxgraph_api/src/app/core/auth/authorization.py +379 -0
- tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
- tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
- tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
- tenxgraph_api/src/app/core/auth/permissions.py +354 -0
- tenxgraph_api/src/app/core/auth/request_config.py +130 -0
- tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
- tenxgraph_api/src/app/core/config/__init__.py +0 -0
- tenxgraph_api/src/app/core/config/graph_config.py +532 -0
- tenxgraph_api/src/app/core/config/media_settings.py +72 -0
- tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
- tenxgraph_api/src/app/core/config/settings.py +202 -0
- tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
- tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
- tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
- tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
- tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
- tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
- tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
- tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
- tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
- tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
- tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
- tenxgraph_api/src/app/core/utils/__init__.py +14 -0
- tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
- tenxgraph_api/src/app/loader.py +516 -0
- tenxgraph_api/src/app/main.py +180 -0
- tenxgraph_api/src/app/routers/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
- tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
- tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
- tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
- tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
- tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
- tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
- tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
- tenxgraph_api/src/app/routers/evals/router.py +38 -0
- tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
- tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
- tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
- tenxgraph_api/src/app/routers/graph/router.py +821 -0
- tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
- tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
- tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
- tenxgraph_api/src/app/routers/media/__init__.py +506 -0
- tenxgraph_api/src/app/routers/media/router.py +242 -0
- tenxgraph_api/src/app/routers/media/schemas.py +39 -0
- tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ping/router.py +28 -0
- tenxgraph_api/src/app/routers/setup_router.py +60 -0
- tenxgraph_api/src/app/routers/store/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/router.py +200 -0
- tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
- tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
- tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
- tenxgraph_api/src/app/tasks/__init__.py +0 -0
- tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
- tenxgraph_api/src/app/utils/__init__.py +12 -0
- tenxgraph_api/src/app/utils/callable_helper.py +26 -0
- tenxgraph_api/src/app/utils/media/__init__.py +15 -0
- tenxgraph_api/src/app/utils/media/extractor.py +172 -0
- tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
- tenxgraph_api/src/app/utils/parse_output.py +17 -0
- tenxgraph_api/src/app/utils/response_helper.py +129 -0
- tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
- tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
- tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
- tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
- tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
- tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
- tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
- 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,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
|