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.
Files changed (137) hide show
  1. qtype/application/commons/tools.py +1 -1
  2. qtype/application/converters/tools_from_api.py +476 -11
  3. qtype/application/converters/tools_from_module.py +38 -14
  4. qtype/application/converters/types.py +15 -30
  5. qtype/application/documentation.py +1 -1
  6. qtype/application/facade.py +102 -85
  7. qtype/base/types.py +227 -7
  8. qtype/cli.py +5 -1
  9. qtype/commands/convert.py +52 -6
  10. qtype/commands/generate.py +44 -4
  11. qtype/commands/run.py +78 -36
  12. qtype/commands/serve.py +74 -44
  13. qtype/commands/validate.py +37 -14
  14. qtype/commands/visualize.py +46 -25
  15. qtype/dsl/__init__.py +6 -5
  16. qtype/dsl/custom_types.py +1 -1
  17. qtype/dsl/domain_types.py +86 -5
  18. qtype/dsl/linker.py +384 -0
  19. qtype/dsl/loader.py +315 -0
  20. qtype/dsl/model.py +753 -264
  21. qtype/dsl/parser.py +200 -0
  22. qtype/dsl/types.py +50 -0
  23. qtype/interpreter/api.py +63 -136
  24. qtype/interpreter/auth/aws.py +19 -9
  25. qtype/interpreter/auth/generic.py +93 -16
  26. qtype/interpreter/base/base_step_executor.py +436 -0
  27. qtype/interpreter/base/batch_step_executor.py +171 -0
  28. qtype/interpreter/base/exceptions.py +50 -0
  29. qtype/interpreter/base/executor_context.py +91 -0
  30. qtype/interpreter/base/factory.py +84 -0
  31. qtype/interpreter/base/progress_tracker.py +110 -0
  32. qtype/interpreter/base/secrets.py +339 -0
  33. qtype/interpreter/base/step_cache.py +74 -0
  34. qtype/interpreter/base/stream_emitter.py +469 -0
  35. qtype/interpreter/conversions.py +495 -24
  36. qtype/interpreter/converters.py +79 -0
  37. qtype/interpreter/endpoints.py +355 -0
  38. qtype/interpreter/executors/agent_executor.py +242 -0
  39. qtype/interpreter/executors/aggregate_executor.py +93 -0
  40. qtype/interpreter/executors/bedrock_reranker_executor.py +195 -0
  41. qtype/interpreter/executors/decoder_executor.py +163 -0
  42. qtype/interpreter/executors/doc_to_text_executor.py +112 -0
  43. qtype/interpreter/executors/document_embedder_executor.py +123 -0
  44. qtype/interpreter/executors/document_search_executor.py +113 -0
  45. qtype/interpreter/executors/document_source_executor.py +118 -0
  46. qtype/interpreter/executors/document_splitter_executor.py +105 -0
  47. qtype/interpreter/executors/echo_executor.py +63 -0
  48. qtype/interpreter/executors/field_extractor_executor.py +165 -0
  49. qtype/interpreter/executors/file_source_executor.py +101 -0
  50. qtype/interpreter/executors/file_writer_executor.py +110 -0
  51. qtype/interpreter/executors/index_upsert_executor.py +232 -0
  52. qtype/interpreter/executors/invoke_embedding_executor.py +104 -0
  53. qtype/interpreter/executors/invoke_flow_executor.py +51 -0
  54. qtype/interpreter/executors/invoke_tool_executor.py +358 -0
  55. qtype/interpreter/executors/llm_inference_executor.py +272 -0
  56. qtype/interpreter/executors/prompt_template_executor.py +78 -0
  57. qtype/interpreter/executors/sql_source_executor.py +106 -0
  58. qtype/interpreter/executors/vector_search_executor.py +91 -0
  59. qtype/interpreter/flow.py +172 -22
  60. qtype/interpreter/logging_progress.py +61 -0
  61. qtype/interpreter/metadata_api.py +115 -0
  62. qtype/interpreter/resource_cache.py +5 -4
  63. qtype/interpreter/rich_progress.py +225 -0
  64. qtype/interpreter/stream/chat/__init__.py +15 -0
  65. qtype/interpreter/stream/chat/converter.py +391 -0
  66. qtype/interpreter/{chat → stream/chat}/file_conversions.py +2 -2
  67. qtype/interpreter/stream/chat/ui_request_to_domain_type.py +140 -0
  68. qtype/interpreter/stream/chat/vercel.py +609 -0
  69. qtype/interpreter/stream/utils/__init__.py +15 -0
  70. qtype/interpreter/stream/utils/build_vercel_ai_formatter.py +74 -0
  71. qtype/interpreter/stream/utils/callback_to_stream.py +66 -0
  72. qtype/interpreter/stream/utils/create_streaming_response.py +18 -0
  73. qtype/interpreter/stream/utils/default_chat_extract_text.py +20 -0
  74. qtype/interpreter/stream/utils/error_streaming_response.py +20 -0
  75. qtype/interpreter/telemetry.py +135 -8
  76. qtype/interpreter/tools/__init__.py +5 -0
  77. qtype/interpreter/tools/function_tool_helper.py +265 -0
  78. qtype/interpreter/types.py +330 -0
  79. qtype/interpreter/typing.py +83 -89
  80. qtype/interpreter/ui/404/index.html +1 -1
  81. qtype/interpreter/ui/404.html +1 -1
  82. qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_buildManifest.js +1 -1
  83. qtype/interpreter/ui/_next/static/chunks/434-b2112d19f25c44ff.js +36 -0
  84. qtype/interpreter/ui/_next/static/chunks/{964-ed4ab073db645007.js → 964-2b041321a01cbf56.js} +1 -1
  85. qtype/interpreter/ui/_next/static/chunks/app/{layout-5ccbc44fd528d089.js → layout-a05273ead5de2c41.js} +1 -1
  86. qtype/interpreter/ui/_next/static/chunks/app/page-8c67d16ac90d23cb.js +1 -0
  87. qtype/interpreter/ui/_next/static/chunks/ba12c10f-546f2714ff8abc66.js +1 -0
  88. qtype/interpreter/ui/_next/static/chunks/{main-6d261b6c5d6fb6c2.js → main-e26b9cb206da2cac.js} +1 -1
  89. qtype/interpreter/ui/_next/static/chunks/webpack-08642e441b39b6c2.js +1 -0
  90. qtype/interpreter/ui/_next/static/css/8a8d1269e362fef7.css +3 -0
  91. qtype/interpreter/ui/_next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  92. qtype/interpreter/ui/icon.png +0 -0
  93. qtype/interpreter/ui/index.html +1 -1
  94. qtype/interpreter/ui/index.txt +5 -5
  95. qtype/semantic/checker.py +643 -0
  96. qtype/semantic/generate.py +268 -85
  97. qtype/semantic/loader.py +95 -0
  98. qtype/semantic/model.py +535 -163
  99. qtype/semantic/resolver.py +63 -19
  100. qtype/semantic/visualize.py +50 -35
  101. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/METADATA +22 -5
  102. qtype-0.1.7.dist-info/RECORD +137 -0
  103. qtype/dsl/base_types.py +0 -38
  104. qtype/dsl/validator.py +0 -464
  105. qtype/interpreter/batch/__init__.py +0 -0
  106. qtype/interpreter/batch/flow.py +0 -95
  107. qtype/interpreter/batch/sql_source.py +0 -95
  108. qtype/interpreter/batch/step.py +0 -63
  109. qtype/interpreter/batch/types.py +0 -41
  110. qtype/interpreter/batch/utils.py +0 -179
  111. qtype/interpreter/chat/chat_api.py +0 -237
  112. qtype/interpreter/chat/vercel.py +0 -314
  113. qtype/interpreter/exceptions.py +0 -10
  114. qtype/interpreter/step.py +0 -67
  115. qtype/interpreter/steps/__init__.py +0 -0
  116. qtype/interpreter/steps/agent.py +0 -114
  117. qtype/interpreter/steps/condition.py +0 -36
  118. qtype/interpreter/steps/decoder.py +0 -88
  119. qtype/interpreter/steps/llm_inference.py +0 -150
  120. qtype/interpreter/steps/prompt_template.py +0 -54
  121. qtype/interpreter/steps/search.py +0 -24
  122. qtype/interpreter/steps/tool.py +0 -53
  123. qtype/interpreter/streaming_helpers.py +0 -123
  124. qtype/interpreter/ui/_next/static/chunks/736-7fc606e244fedcb1.js +0 -36
  125. qtype/interpreter/ui/_next/static/chunks/app/page-c72e847e888e549d.js +0 -1
  126. qtype/interpreter/ui/_next/static/chunks/ba12c10f-22556063851a6df2.js +0 -1
  127. qtype/interpreter/ui/_next/static/chunks/webpack-8289c17c67827f22.js +0 -1
  128. qtype/interpreter/ui/_next/static/css/a262c53826df929b.css +0 -3
  129. qtype/interpreter/ui/_next/static/media/569ce4b8f30dc480-s.p.woff2 +0 -0
  130. qtype/interpreter/ui/favicon.ico +0 -0
  131. qtype/loader.py +0 -389
  132. qtype-0.0.12.dist-info/RECORD +0 -105
  133. /qtype/interpreter/ui/_next/static/{OT8QJQW3J70VbDWWfrEMT → 20HoJN6otZ_LyHLHpCPE6}/_ssgManifest.js +0 -0
  134. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/WHEEL +0 -0
  135. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/entry_points.txt +0 -0
  136. {qtype-0.0.12.dist-info → qtype-0.1.7.dist-info}/licenses/LICENSE +0 -0
  137. {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