qtype 0.0.12__py3-none-any.whl → 0.1.7__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.
- qtype/application/commons/tools.py +1 -1
- qtype/application/converters/tools_from_api.py +476 -11
- qtype/application/converters/tools_from_module.py +38 -14
- qtype/application/converters/types.py +15 -30
- qtype/application/documentation.py +1 -1
- qtype/application/facade.py +102 -85
- qtype/base/types.py +227 -7
- qtype/cli.py +5 -1
- qtype/commands/convert.py +52 -6
- qtype/commands/generate.py +44 -4
- qtype/commands/run.py +78 -36
- qtype/commands/serve.py +74 -44
- qtype/commands/validate.py +37 -14
- qtype/commands/visualize.py +46 -25
- qtype/dsl/__init__.py +6 -5
- qtype/dsl/custom_types.py +1 -1
- qtype/dsl/domain_types.py +86 -5
- qtype/dsl/linker.py +384 -0
- qtype/dsl/loader.py +315 -0
- qtype/dsl/model.py +753 -264
- qtype/dsl/parser.py +200 -0
- qtype/dsl/types.py +50 -0
- qtype/interpreter/api.py +63 -136
- qtype/interpreter/auth/aws.py +19 -9
- qtype/interpreter/auth/generic.py +93 -16
- qtype/interpreter/base/base_step_executor.py +436 -0
- qtype/interpreter/base/batch_step_executor.py +171 -0
- qtype/interpreter/base/exceptions.py +50 -0
- qtype/interpreter/base/executor_context.py +91 -0
- qtype/interpreter/base/factory.py +84 -0
- qtype/interpreter/base/progress_tracker.py +110 -0
- qtype/interpreter/base/secrets.py +339 -0
- qtype/interpreter/base/step_cache.py +74 -0
- qtype/interpreter/base/stream_emitter.py +469 -0
- qtype/interpreter/conversions.py +495 -24
- qtype/interpreter/converters.py +79 -0
- qtype/interpreter/endpoints.py +355 -0
- qtype/interpreter/executors/agent_executor.py +242 -0
- qtype/interpreter/executors/aggregate_executor.py +93 -0
- qtype/interpreter/executors/bedrock_reranker_executor.py +195 -0
- qtype/interpreter/executors/decoder_executor.py +163 -0
- qtype/interpreter/executors/doc_to_text_executor.py +112 -0
- qtype/interpreter/executors/document_embedder_executor.py +123 -0
- qtype/interpreter/executors/document_search_executor.py +113 -0
- qtype/interpreter/executors/document_source_executor.py +118 -0
- qtype/interpreter/executors/document_splitter_executor.py +105 -0
- qtype/interpreter/executors/echo_executor.py +63 -0
- qtype/interpreter/executors/field_extractor_executor.py +165 -0
- qtype/interpreter/executors/file_source_executor.py +101 -0
- qtype/interpreter/executors/file_writer_executor.py +110 -0
- qtype/interpreter/executors/index_upsert_executor.py +232 -0
- qtype/interpreter/executors/invoke_embedding_executor.py +104 -0
- qtype/interpreter/executors/invoke_flow_executor.py +51 -0
- qtype/interpreter/executors/invoke_tool_executor.py +358 -0
- qtype/interpreter/executors/llm_inference_executor.py +272 -0
- qtype/interpreter/executors/prompt_template_executor.py +78 -0
- qtype/interpreter/executors/sql_source_executor.py +106 -0
- qtype/interpreter/executors/vector_search_executor.py +91 -0
- qtype/interpreter/flow.py +172 -22
- qtype/interpreter/logging_progress.py +61 -0
- qtype/interpreter/metadata_api.py +115 -0
- qtype/interpreter/resource_cache.py +5 -4
- qtype/interpreter/rich_progress.py +225 -0
- qtype/interpreter/stream/chat/__init__.py +15 -0
- qtype/interpreter/stream/chat/converter.py +391 -0
- qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
- qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
- qtype/interpreter/stream/chat/vercel.py +609 -0
- qtype/interpreter/stream/utils/__init__.py +15 -0
- qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
- qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
- qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
- qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
- qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
- qtype/interpreter/telemetry.py +135 -8
- qtype/interpreter/tools/__init__.py +5 -0
- qtype/interpreter/tools/function_tool_helper.py +265 -0
- qtype/interpreter/types.py +330 -0
- qtype/interpreter/typing.py +83 -89
- qtype/interpreter/ui/404/index.html +1 -1
- qtype/interpreter/ui/404.html +1 -1
- qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
- qtype/interpreter/ui/_next/static/chunks/434-b2112d19f25c44ff.js +36 -0
- qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
- qtype/interpreter/ui/_next/static/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
- qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
- qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
- qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
- qtype/interpreter/ui/icon.png +0 -0
- qtype/interpreter/ui/index.html +1 -1
- qtype/interpreter/ui/index.txt +5 -5
- qtype/semantic/checker.py +643 -0
- qtype/semantic/generate.py +268 -85
- qtype/semantic/loader.py +95 -0
- qtype/semantic/model.py +535 -163
- qtype/semantic/resolver.py +63 -19
- qtype/semantic/visualize.py +50 -35
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/METADATA +22 -5
- qtype-0.1.7.dist-info/RECORD +137 -0
- qtype/dsl/base_types.py +0 -38
- qtype/dsl/validator.py +0 -464
- qtype/interpreter/batch/__init__.py +0 -0
- qtype/interpreter/batch/flow.py +0 -95
- qtype/interpreter/batch/sql_source.py +0 -95
- qtype/interpreter/batch/step.py +0 -63
- qtype/interpreter/batch/types.py +0 -41
- qtype/interpreter/batch/utils.py +0 -179
- qtype/interpreter/chat/chat_api.py +0 -237
- qtype/interpreter/chat/vercel.py +0 -314
- qtype/interpreter/exceptions.py +0 -10
- qtype/interpreter/step.py +0 -67
- qtype/interpreter/steps/__init__.py +0 -0
- qtype/interpreter/steps/agent.py +0 -114
- qtype/interpreter/steps/condition.py +0 -36
- qtype/interpreter/steps/decoder.py +0 -88
- qtype/interpreter/steps/llm_inference.py +0 -150
- qtype/interpreter/steps/prompt_template.py +0 -54
- qtype/interpreter/steps/search.py +0 -24
- qtype/interpreter/steps/tool.py +0 -53
- qtype/interpreter/streaming_helpers.py +0 -123
- qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
- qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
- qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
- qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
- qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
- qtype/interpreter/ui/favicon.ico +0 -0
- qtype/loader.py +0 -389
- qtype-0.0.12.dist-info/RECORD +0 -105
- /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/WHEEL +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/entry_points.txt +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""BedrockReranker executor for reordering search results by relevance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import AsyncIterator
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from qtype.base.types import PrimitiveTypeEnum
|
|
12
|
+
from qtype.dsl.domain_types import RAGChunk, SearchResult
|
|
13
|
+
from qtype.interpreter.auth.aws import aws
|
|
14
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
15
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
16
|
+
from qtype.interpreter.types import FlowMessage
|
|
17
|
+
from qtype.semantic.model import BedrockReranker, ListType
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BedrockRerankerExecutor(StepExecutor):
|
|
23
|
+
"""Executor for BedrockReranker steps that reorder search results by relevance."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self, step: BedrockReranker, context: ExecutorContext, **dependencies
|
|
27
|
+
):
|
|
28
|
+
super().__init__(step, context, **dependencies)
|
|
29
|
+
if not isinstance(step, BedrockReranker):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"BedrockRerankerExecutor can only execute BedrockReranker steps."
|
|
32
|
+
)
|
|
33
|
+
self.step: BedrockReranker = step
|
|
34
|
+
|
|
35
|
+
async def process_message(
|
|
36
|
+
self,
|
|
37
|
+
message: FlowMessage,
|
|
38
|
+
) -> AsyncIterator[FlowMessage]:
|
|
39
|
+
"""Process a single FlowMessage for the BedrockReranker step.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
message: The FlowMessage to process.
|
|
43
|
+
|
|
44
|
+
Yields:
|
|
45
|
+
FlowMessage with reranked results.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
# Get the inputs
|
|
49
|
+
query = self._query(message)
|
|
50
|
+
docs = self._docs(message)
|
|
51
|
+
|
|
52
|
+
if len(docs) == 0:
|
|
53
|
+
# No documents to rerank, yield original message
|
|
54
|
+
yield message.copy_with_variables(
|
|
55
|
+
{self.step.outputs[0].id: docs}
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
# Get session for region info
|
|
60
|
+
if self.step.auth is not None:
|
|
61
|
+
with aws(self.step.auth, self.context.secret_manager) as s:
|
|
62
|
+
region_name = s.region_name
|
|
63
|
+
else:
|
|
64
|
+
import boto3
|
|
65
|
+
|
|
66
|
+
region_name = boto3.Session().region_name
|
|
67
|
+
|
|
68
|
+
# Convert the types
|
|
69
|
+
queries = [
|
|
70
|
+
{
|
|
71
|
+
"type": "TEXT",
|
|
72
|
+
"textQuery": {"text": query},
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
documents = []
|
|
76
|
+
|
|
77
|
+
for doc in docs:
|
|
78
|
+
if isinstance(doc.content, RAGChunk):
|
|
79
|
+
documents.append(
|
|
80
|
+
{
|
|
81
|
+
"type": "INLINE",
|
|
82
|
+
"inlineDocumentSource": {
|
|
83
|
+
"type": "TEXT",
|
|
84
|
+
"textDocument": {"text": str(doc.content)},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
elif isinstance(doc.content, dict):
|
|
89
|
+
documents.append(
|
|
90
|
+
{
|
|
91
|
+
"type": "INLINE",
|
|
92
|
+
"inlineDocumentSource": {
|
|
93
|
+
"type": "JSON",
|
|
94
|
+
"jsonDocument": doc.content,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
elif isinstance(doc.content, BaseModel):
|
|
99
|
+
documents.append(
|
|
100
|
+
{
|
|
101
|
+
"type": "INLINE",
|
|
102
|
+
"inlineDocumentSource": {
|
|
103
|
+
"type": "JSON",
|
|
104
|
+
"jsonDocument": doc.content.model_dump(),
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Unsupported document content type for BedrockReranker: {type(doc.content)}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
reranking_configuration = {
|
|
114
|
+
"type": "BEDROCK_RERANKING_MODEL",
|
|
115
|
+
"bedrockRerankingConfiguration": {
|
|
116
|
+
"numberOfResults": self.step.num_results or len(docs),
|
|
117
|
+
"modelConfiguration": {
|
|
118
|
+
"modelArn": f"arn:aws:bedrock:{region_name}::foundation-model/{self.step.model_id}"
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
def _call_bedrock_rerank():
|
|
124
|
+
"""Create client and call rerank in executor thread."""
|
|
125
|
+
if self.step.auth is not None:
|
|
126
|
+
with aws(self.step.auth, self.context.secret_manager) as s:
|
|
127
|
+
client = s.client("bedrock-agent-runtime")
|
|
128
|
+
return client.rerank(
|
|
129
|
+
queries=queries,
|
|
130
|
+
sources=documents,
|
|
131
|
+
rerankingConfiguration=reranking_configuration,
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
import boto3
|
|
135
|
+
|
|
136
|
+
session = boto3.Session()
|
|
137
|
+
client = session.client("bedrock-agent-runtime")
|
|
138
|
+
return client.rerank(
|
|
139
|
+
queries=queries,
|
|
140
|
+
sources=documents,
|
|
141
|
+
rerankingConfiguration=reranking_configuration,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
loop = asyncio.get_running_loop()
|
|
145
|
+
response = await loop.run_in_executor(
|
|
146
|
+
self.context.thread_pool, _call_bedrock_rerank
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
results = []
|
|
150
|
+
for d in response["results"]:
|
|
151
|
+
doc = docs[d["index"]]
|
|
152
|
+
new_score = d["relevanceScore"]
|
|
153
|
+
results.append(doc.copy(update={"score": new_score}))
|
|
154
|
+
|
|
155
|
+
# Update the message with reranked results
|
|
156
|
+
yield message.copy_with_variables(
|
|
157
|
+
{self.step.outputs[0].id: results}
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"Reranking failed: {e}", exc_info=True)
|
|
161
|
+
# Emit error event to stream so frontend can display it
|
|
162
|
+
await self.stream_emitter.error(str(e))
|
|
163
|
+
message.set_error(self.step.id, e)
|
|
164
|
+
yield message
|
|
165
|
+
|
|
166
|
+
def _query(self, message: FlowMessage) -> str:
|
|
167
|
+
"""Extract the query string from the FlowMessage.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
message: The FlowMessage containing the query variable.
|
|
171
|
+
Returns:
|
|
172
|
+
The query string.
|
|
173
|
+
"""
|
|
174
|
+
for i in self.step.inputs:
|
|
175
|
+
if i.type == PrimitiveTypeEnum.text:
|
|
176
|
+
return message.variables[i.id]
|
|
177
|
+
raise ValueError(
|
|
178
|
+
f"No text input found for BedrockReranker step {self.step.id}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _docs(self, message: FlowMessage) -> list[SearchResult]:
|
|
182
|
+
"""Extract the list of SearchResult documents from the FlowMessage.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
message: The FlowMessage containing the document variable.
|
|
186
|
+
Returns:
|
|
187
|
+
The list of SearchResult documents.
|
|
188
|
+
"""
|
|
189
|
+
for i in self.step.inputs:
|
|
190
|
+
if i.type == ListType(element_type="SearchResult"):
|
|
191
|
+
docs = message.variables[i.id]
|
|
192
|
+
return docs
|
|
193
|
+
raise ValueError(
|
|
194
|
+
f"No list of SearchResults input found for BedrockReranker step {self.step.id}"
|
|
195
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
from typing import Any, AsyncIterator
|
|
4
|
+
|
|
5
|
+
from qtype.dsl.model import DecoderFormat
|
|
6
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
7
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
8
|
+
from qtype.interpreter.types import FlowMessage
|
|
9
|
+
from qtype.semantic.model import Decoder
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DecoderExecutor(StepExecutor):
|
|
13
|
+
"""Executor for Decoder steps."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, step: Decoder, context: ExecutorContext, **dependencies
|
|
17
|
+
):
|
|
18
|
+
super().__init__(step, context, **dependencies)
|
|
19
|
+
if not isinstance(step, Decoder):
|
|
20
|
+
raise ValueError("DecoderExecutor can only execute Decoder steps.")
|
|
21
|
+
self.step: Decoder = step
|
|
22
|
+
|
|
23
|
+
def _parse_json(self, input_str: str) -> dict[str, Any]:
|
|
24
|
+
"""Parse a JSON string into a Python object.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
input_str: The JSON string to parse.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A dictionary parsed from the JSON.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: If the JSON is invalid or not an object.
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
cleaned_response = input_str.strip()
|
|
37
|
+
# Remove markdown code fences if present
|
|
38
|
+
if cleaned_response.startswith("```json"):
|
|
39
|
+
cleaned_response = cleaned_response[7:]
|
|
40
|
+
if cleaned_response.endswith("```"):
|
|
41
|
+
cleaned_response = cleaned_response[:-3]
|
|
42
|
+
cleaned_response = cleaned_response.strip()
|
|
43
|
+
|
|
44
|
+
# Parse the JSON
|
|
45
|
+
parsed = json.loads(cleaned_response)
|
|
46
|
+
if not isinstance(parsed, dict):
|
|
47
|
+
raise ValueError(f"Parsed JSON is not an object: {parsed}")
|
|
48
|
+
return parsed
|
|
49
|
+
except json.JSONDecodeError as e:
|
|
50
|
+
raise ValueError(f"Invalid JSON input: {e}") from e
|
|
51
|
+
|
|
52
|
+
def _parse_xml(self, input_str: str) -> dict[str, Any]:
|
|
53
|
+
"""Parse an XML string into a Python object.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
input_str: The XML string to parse.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
A dictionary with tag names as keys and text content as values.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If the XML is invalid.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
cleaned_response = input_str.strip()
|
|
66
|
+
# Remove markdown code fences if present
|
|
67
|
+
if cleaned_response.startswith("```xml"):
|
|
68
|
+
cleaned_response = cleaned_response[6:]
|
|
69
|
+
if cleaned_response.endswith("```"):
|
|
70
|
+
cleaned_response = cleaned_response[:-3]
|
|
71
|
+
cleaned_response = cleaned_response.strip()
|
|
72
|
+
|
|
73
|
+
# Escape ampersands
|
|
74
|
+
cleaned_response = cleaned_response.replace("&", "&")
|
|
75
|
+
tree = ET.fromstring(cleaned_response)
|
|
76
|
+
result = {c.tag: c.text for c in tree}
|
|
77
|
+
|
|
78
|
+
return result
|
|
79
|
+
except Exception as e:
|
|
80
|
+
raise ValueError(f"Invalid XML input: {e}") from e
|
|
81
|
+
|
|
82
|
+
def _parse(self, input_str: str) -> dict[str, Any]:
|
|
83
|
+
"""Parse input string based on the decoder format.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
input_str: The string to parse.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A dictionary parsed from the input.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If the format is unsupported or parsing fails.
|
|
93
|
+
"""
|
|
94
|
+
if self.step.format == DecoderFormat.json:
|
|
95
|
+
return self._parse_json(input_str)
|
|
96
|
+
elif self.step.format == DecoderFormat.xml:
|
|
97
|
+
return self._parse_xml(input_str)
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
(
|
|
101
|
+
f"Unsupported decoder format: {self.step.format}. "
|
|
102
|
+
f"Supported formats are: {DecoderFormat.json}, "
|
|
103
|
+
f"{DecoderFormat.xml}."
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def process_message(
|
|
108
|
+
self,
|
|
109
|
+
message: FlowMessage,
|
|
110
|
+
) -> AsyncIterator[FlowMessage]:
|
|
111
|
+
"""Process a single FlowMessage for the Decoder step.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
message: The FlowMessage to process.
|
|
115
|
+
|
|
116
|
+
Yields:
|
|
117
|
+
A FlowMessage with decoded outputs or an error.
|
|
118
|
+
"""
|
|
119
|
+
input_id = self.step.inputs[0].id
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
# Get the input string to decode
|
|
123
|
+
input_value = message.variables.get(input_id)
|
|
124
|
+
if not isinstance(input_value, str):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
(
|
|
127
|
+
f"Input to decoder step {self.step.id} must be "
|
|
128
|
+
f"a string, found {type(input_value).__name__}."
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await self.stream_emitter.status(
|
|
133
|
+
f"Decoding {self.step.format.value} input"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Parse the input
|
|
137
|
+
result_dict = self._parse(input_value)
|
|
138
|
+
|
|
139
|
+
# Extract output variables from the parsed result
|
|
140
|
+
output_vars = {}
|
|
141
|
+
for output in self.step.outputs:
|
|
142
|
+
if output.id in result_dict:
|
|
143
|
+
output_vars[output.id] = result_dict[output.id]
|
|
144
|
+
else:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
(
|
|
147
|
+
f"Output variable {output.id} not found in "
|
|
148
|
+
f"decoded result: {result_dict}"
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
await self.stream_emitter.status(
|
|
153
|
+
f"Decoded {len(output_vars)} output variables"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Yield the result
|
|
157
|
+
yield message.copy_with_variables(output_vars)
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
# Emit error event to stream so frontend can display it
|
|
161
|
+
await self.stream_emitter.error(str(e))
|
|
162
|
+
message.set_error(self.step.id, e)
|
|
163
|
+
yield message
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from io import BytesIO
|
|
2
|
+
from typing import AsyncIterator
|
|
3
|
+
|
|
4
|
+
from docling.document_converter import DocumentConverter
|
|
5
|
+
from docling_core.types.io import DocumentStream
|
|
6
|
+
|
|
7
|
+
from qtype.base.types import PrimitiveTypeEnum
|
|
8
|
+
from qtype.dsl.domain_types import RAGDocument
|
|
9
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
10
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
11
|
+
from qtype.interpreter.types import FlowMessage
|
|
12
|
+
from qtype.semantic.model import DocToTextConverter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DocToTextConverterExecutor(StepExecutor):
|
|
16
|
+
"""Executor for DocToTextConverter steps."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
step: DocToTextConverter,
|
|
21
|
+
context: ExecutorContext,
|
|
22
|
+
**dependencies,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(step, context, **dependencies)
|
|
25
|
+
if not isinstance(step, DocToTextConverter):
|
|
26
|
+
raise ValueError(
|
|
27
|
+
(
|
|
28
|
+
"DocToTextConverterExecutor can only execute "
|
|
29
|
+
"DocToTextConverter steps."
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
self.step: DocToTextConverter = step
|
|
33
|
+
# Initialize the Docling converter once for the executor
|
|
34
|
+
self.docling_converter = DocumentConverter()
|
|
35
|
+
|
|
36
|
+
async def process_message(
|
|
37
|
+
self,
|
|
38
|
+
message: FlowMessage,
|
|
39
|
+
) -> AsyncIterator[FlowMessage]:
|
|
40
|
+
"""Process a single FlowMessage for the DocToTextConverter step.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
message: The FlowMessage to process.
|
|
44
|
+
Yields:
|
|
45
|
+
FlowMessage with converted document.
|
|
46
|
+
"""
|
|
47
|
+
input_id = self.step.inputs[0].id
|
|
48
|
+
output_id = self.step.outputs[0].id
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
# Get the input document
|
|
52
|
+
if input_id not in message.variables:
|
|
53
|
+
raise ValueError(f"Input variable '{input_id}' is missing")
|
|
54
|
+
doc = message.variables.get(input_id)
|
|
55
|
+
if not isinstance(doc, RAGDocument):
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Input variable '{input_id}' must be a RAGDocument"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
await self.stream_emitter.status(
|
|
61
|
+
f"Converting document: {doc.file_name}",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Convert the document
|
|
65
|
+
converted_doc = self._convert_doc(doc)
|
|
66
|
+
|
|
67
|
+
await self.stream_emitter.status(
|
|
68
|
+
f"Converted {doc.file_name} to markdown text",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Yield the result
|
|
72
|
+
yield message.copy_with_variables({output_id: converted_doc})
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
# Emit error event to stream so frontend can display it
|
|
76
|
+
await self.stream_emitter.error(str(e))
|
|
77
|
+
message.set_error(self.step.id, e)
|
|
78
|
+
yield message
|
|
79
|
+
|
|
80
|
+
def _convert_doc(self, doc: RAGDocument) -> RAGDocument:
|
|
81
|
+
"""Convert a RAGDocument to text/markdown format.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
doc: The document to convert.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
A RAGDocument with markdown text content.
|
|
88
|
+
"""
|
|
89
|
+
# If already text, no conversion needed
|
|
90
|
+
if doc.type == PrimitiveTypeEnum.text:
|
|
91
|
+
return doc
|
|
92
|
+
|
|
93
|
+
# Convert based on content type
|
|
94
|
+
if isinstance(doc.content, bytes):
|
|
95
|
+
# Use DocumentStream for bytes content
|
|
96
|
+
stream = DocumentStream(
|
|
97
|
+
name=doc.file_name, stream=BytesIO(doc.content)
|
|
98
|
+
)
|
|
99
|
+
document = self.docling_converter.convert(stream).document
|
|
100
|
+
else:
|
|
101
|
+
# Convert string content directly
|
|
102
|
+
document = self.docling_converter.convert(doc.content).document
|
|
103
|
+
|
|
104
|
+
# Export to markdown
|
|
105
|
+
markdown = document.export_to_markdown()
|
|
106
|
+
|
|
107
|
+
# Return new RAGDocument with markdown content
|
|
108
|
+
return RAGDocument(
|
|
109
|
+
**doc.model_dump(exclude={"content", "type"}),
|
|
110
|
+
content=markdown,
|
|
111
|
+
type=PrimitiveTypeEnum.text,
|
|
112
|
+
)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import AsyncIterator
|
|
4
|
+
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
from llama_index.core.base.embeddings.base import BaseEmbedding
|
|
7
|
+
from tenacity import (
|
|
8
|
+
retry,
|
|
9
|
+
retry_if_exception,
|
|
10
|
+
stop_after_attempt,
|
|
11
|
+
wait_exponential,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from qtype.dsl.domain_types import RAGChunk
|
|
15
|
+
from qtype.interpreter.base.base_step_executor import StepExecutor
|
|
16
|
+
from qtype.interpreter.base.executor_context import ExecutorContext
|
|
17
|
+
from qtype.interpreter.conversions import to_embedding_model
|
|
18
|
+
from qtype.interpreter.types import FlowMessage
|
|
19
|
+
from qtype.semantic.model import DocumentEmbedder
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_throttling_error(e):
|
|
23
|
+
return (
|
|
24
|
+
isinstance(e, ClientError)
|
|
25
|
+
and e.response["Error"]["Code"] == "ThrottlingException"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DocumentEmbedderExecutor(StepExecutor):
|
|
30
|
+
"""Executor for DocumentEmbedder steps."""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self, step: DocumentEmbedder, context: ExecutorContext, **dependencies
|
|
34
|
+
):
|
|
35
|
+
super().__init__(step, context, **dependencies)
|
|
36
|
+
if not isinstance(step, DocumentEmbedder):
|
|
37
|
+
raise ValueError(
|
|
38
|
+
(
|
|
39
|
+
"DocumentEmbedderExecutor can only execute "
|
|
40
|
+
"DocumentEmbedder steps."
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
self.step: DocumentEmbedder = step
|
|
44
|
+
# Initialize the embedding model once for the executor
|
|
45
|
+
self.embedding_model: BaseEmbedding = to_embedding_model(
|
|
46
|
+
self.step.model, context.secret_manager
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# TODO: properly abstract this into a mixin
|
|
50
|
+
@retry(
|
|
51
|
+
retry=retry_if_exception(is_throttling_error),
|
|
52
|
+
wait=wait_exponential(multiplier=0.5, min=1, max=30),
|
|
53
|
+
stop=stop_after_attempt(10),
|
|
54
|
+
)
|
|
55
|
+
async def _embed(self, text: str) -> list[float]:
|
|
56
|
+
"""Generate embedding for the given text using the embedding model.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
text: The text to embed.
|
|
60
|
+
Returns:
|
|
61
|
+
The embedding vector as a list of floats.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
# TODO: switch back to async once aws auth supports it.
|
|
65
|
+
# https://github.com/bazaarvoice/qtype/issues/108
|
|
66
|
+
def _call():
|
|
67
|
+
return self.embedding_model.get_text_embedding(text=text)
|
|
68
|
+
|
|
69
|
+
loop = asyncio.get_running_loop()
|
|
70
|
+
response = await loop.run_in_executor(self.context.thread_pool, _call)
|
|
71
|
+
|
|
72
|
+
return response
|
|
73
|
+
# return await self.embedding_model.aget_text_embedding(text=text)
|
|
74
|
+
|
|
75
|
+
async def process_message(
|
|
76
|
+
self,
|
|
77
|
+
message: FlowMessage,
|
|
78
|
+
) -> AsyncIterator[FlowMessage]:
|
|
79
|
+
"""Process a single FlowMessage for the DocumentEmbedder step.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
message: The FlowMessage to process.
|
|
83
|
+
Yields:
|
|
84
|
+
FlowMessage with embedded chunk.
|
|
85
|
+
"""
|
|
86
|
+
input_id = self.step.inputs[0].id
|
|
87
|
+
output_id = self.step.outputs[0].id
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Get the input chunk
|
|
91
|
+
chunk = message.variables.get(input_id)
|
|
92
|
+
if not isinstance(chunk, RAGChunk):
|
|
93
|
+
raise ValueError(
|
|
94
|
+
(
|
|
95
|
+
f"Input variable '{input_id}' must be a RAGChunk, "
|
|
96
|
+
f"got {type(chunk)}"
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Generate embedding for the chunk content
|
|
101
|
+
vector = await self._embed(str(chunk.content))
|
|
102
|
+
|
|
103
|
+
# Create the output chunk with the vector
|
|
104
|
+
embedded_chunk = RAGChunk(
|
|
105
|
+
vector=vector,
|
|
106
|
+
content=chunk.content,
|
|
107
|
+
chunk_id=chunk.chunk_id,
|
|
108
|
+
document_id=chunk.document_id,
|
|
109
|
+
metadata=chunk.metadata,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Yield the result
|
|
113
|
+
yield message.copy_with_variables({output_id: embedded_chunk})
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
# Emit error event to stream so frontend can display it
|
|
117
|
+
await self.stream_emitter.error(str(e))
|
|
118
|
+
logging.error(
|
|
119
|
+
f"Error processing DocumentEmbedder step {self.step.id}",
|
|
120
|
+
exc_info=e,
|
|
121
|
+
)
|
|
122
|
+
message.set_error(self.step.id, e)
|
|
123
|
+
yield message
|