qtype 0.1.12__py3-none-any.whl → 0.1.13__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 (252) hide show
  1. qtype/` +0 -0
  2. qtype/application/__init__.py +0 -2
  3. qtype/application/converters/tools_from_api.py +28 -22
  4. qtype/application/converters/tools_from_module.py +66 -32
  5. qtype/commands/generate.py +90 -7
  6. qtype/commands/run.py +116 -44
  7. qtype/docs/.pages +8 -0
  8. {docs → qtype/docs}/Concepts/mental-model-and-philosophy.md +1 -1
  9. qtype/docs/Contributing/.pages +4 -0
  10. {docs → qtype/docs}/Contributing/index.md +8 -1
  11. {docs → qtype/docs}/Gallery/dataflow_pipelines.md +3 -2
  12. {docs → qtype/docs}/Gallery/research_assistant.md +3 -4
  13. {docs → qtype/docs}/Gallery/simple_chatbot.md +3 -1
  14. {docs → qtype/docs}/How To/Authentication/configure_aws_authentication.md +2 -2
  15. {docs → qtype/docs}/How To/Authentication/use_api_key_authentication.md +2 -2
  16. {docs → qtype/docs}/How To/Command Line Usage/load_multiple_inputs_from_files.md +24 -9
  17. {docs → qtype/docs}/How To/Command Line Usage/pass_inputs_on_the_cli.md +3 -3
  18. {docs → qtype/docs}/How To/Command Line Usage/serve_with_auto_reload.md +3 -2
  19. {docs → qtype/docs}/How To/Data Processing/adjust_concurrency.md +3 -4
  20. {docs → qtype/docs}/How To/Data Processing/cache_step_results.md +2 -2
  21. {docs → qtype/docs}/How To/Data Processing/decode_json_xml.md +1 -1
  22. {docs → qtype/docs}/How To/Data Processing/explode_collections.md +2 -2
  23. {docs → qtype/docs}/How To/Data Processing/gather_results.md +4 -4
  24. qtype/docs/How To/Data Processing/invoke_other_flows.md +71 -0
  25. qtype/docs/How To/Data Processing/load_data_from_athena.md +49 -0
  26. qtype/docs/How To/Data Processing/read_data_from_files.md +61 -0
  27. {docs → qtype/docs}/How To/Data Processing/read_sql_databases.md +2 -3
  28. {docs → qtype/docs}/How To/Data Processing/write_data_to_file.md +1 -2
  29. {docs → qtype/docs}/How To/Invoke Models/call_large_language_models.md +1 -1
  30. {docs → qtype/docs}/How To/Invoke Models/create_embeddings.md +1 -1
  31. {docs → qtype/docs}/How To/Invoke Models/reuse_prompts_with_templates.md +2 -3
  32. {docs → qtype/docs}/How To/Language Features/include_raw_text_from_other_files.md +2 -1
  33. {docs → qtype/docs}/How To/Language Features/reference_entities_by_id.md +2 -2
  34. qtype/docs/How To/Language Features/use_agent_skills.md +29 -0
  35. {docs → qtype/docs}/How To/Language Features/use_environment_variables.md +2 -1
  36. qtype/docs/How To/Language Features/use_optional_variables.md +42 -0
  37. {docs → qtype/docs}/How To/Language Features/use_qtype_mcp.md +4 -4
  38. {docs → qtype/docs}/How To/Observability & Debugging/trace_calls_with_open_telemetry.md +1 -1
  39. {docs → qtype/docs}/How To/Observability & Debugging/validate_qtype_yaml.md +3 -2
  40. {docs → qtype/docs}/How To/Observability & Debugging/visualize_application_architecture.md +1 -1
  41. {docs → qtype/docs}/How To/Qtype Server/serve_flows_as_apis.md +3 -3
  42. {docs → qtype/docs}/How To/Qtype Server/serve_flows_as_ui.md +2 -3
  43. {docs → qtype/docs}/How To/Qtype Server/use_conversational_interfaces.md +1 -4
  44. {docs → qtype/docs}/How To/Qtype Server/use_variables_with_ui_hints.md +3 -2
  45. {docs → qtype/docs}/How To/Tools & Integration/bind_tool_inputs_and_outputs.md +1 -2
  46. {docs → qtype/docs}/How To/Tools & Integration/create_tools_from_openapi_specifications.md +10 -14
  47. {docs → qtype/docs}/How To/Tools & Integration/create_tools_from_python_modules.md +5 -8
  48. {docs → qtype/docs}/Reference/cli.md +13 -15
  49. {docs → qtype/docs}/Reference/plugins.md +4 -0
  50. {docs → qtype/docs}/Reference/semantic-validation-rules.md +6 -1
  51. qtype/docs/Tutorials/.pages +1 -0
  52. {docs → qtype/docs}/Tutorials/01-first-qtype-application.md +3 -2
  53. {docs → qtype/docs}/Tutorials/02-conversational-chatbot.md +3 -3
  54. {docs → qtype/docs}/Tutorials/03-structured-data.md +9 -10
  55. {docs → qtype/docs}/Tutorials/04-tools-and-function-calling.md +12 -19
  56. {docs → qtype/docs}/components/APITool.md +1 -1
  57. qtype/docs/components/Aggregate.md +7 -0
  58. qtype/docs/components/Collect.md +6 -0
  59. qtype/docs/components/Construct.md +6 -0
  60. {docs → qtype/docs}/components/DocumentEmbedder.md +0 -1
  61. {docs → qtype/docs}/components/DocumentSplitter.md +0 -1
  62. qtype/docs/components/Explode.md +5 -0
  63. {docs → qtype/docs}/components/FieldExtractor.md +2 -1
  64. qtype/docs/components/InvokeFlow.md +8 -0
  65. qtype/docs/components/InvokeTool.md +8 -0
  66. {docs → qtype/docs}/components/PrimitiveTypeEnum.md +0 -1
  67. {docs → qtype/docs}/components/Source.md +0 -1
  68. {docs → qtype/docs}/components/Step.md +0 -1
  69. {docs → qtype/docs}/components/Tool.md +2 -2
  70. {docs → qtype/docs}/components/Variable.md +2 -0
  71. qtype/docs/legacy_how_tos/.pages +6 -0
  72. qtype/docs/skills/architect/SKILL.md +188 -0
  73. qtype/docs/skills/architect/references/cheatsheet.md +198 -0
  74. qtype/docs/skills/architect/references/patterns.md +29 -0
  75. qtype/docs/stylesheets/extra.css +27 -0
  76. qtype/dsl/linker.py +8 -0
  77. qtype/dsl/model.py +177 -84
  78. qtype/examples/data_processing/athena_query.qtype.yaml +56 -0
  79. qtype/examples/data_processing/batch_inputs.csv +5 -0
  80. qtype/examples/data_processing/create_sample_db.py +129 -0
  81. qtype/examples/data_processing/invoke_other_flows.qtype.yaml +98 -0
  82. qtype/examples/data_processing/reviews.db +0 -0
  83. qtype/examples/data_processing/sample_article.txt +1 -0
  84. qtype/examples/data_processing/sample_documents.jsonl +5 -0
  85. qtype/examples/language_features/optional_variables.qtype.yaml +32 -0
  86. qtype/examples/language_features/story_prompt.txt +6 -0
  87. qtype/examples/legacy/data/customers.csv +6 -0
  88. qtype/examples/legacy/echo/readme.md +29 -0
  89. qtype/examples/legacy/qtype_plugin_example.py +51 -0
  90. qtype/examples/legacy/sample_data.txt +43 -0
  91. qtype/examples/legacy/vertex/README.md +11 -0
  92. qtype/examples/research_assistant/tavily.qtype.yaml +216 -0
  93. {examples → qtype/examples}/tutorials/03_structured_data.qtype.yaml +2 -2
  94. {examples → qtype/examples}/tutorials/04_tools_and_function_calling.qtype.yaml +5 -5
  95. qtype/interpreter/base/stream_emitter.py +19 -13
  96. qtype/interpreter/converters.py +142 -26
  97. qtype/interpreter/executors/agent_executor.py +2 -3
  98. qtype/interpreter/executors/aggregate_executor.py +3 -4
  99. qtype/interpreter/executors/construct_executor.py +15 -15
  100. qtype/interpreter/executors/doc_to_text_executor.py +1 -3
  101. qtype/interpreter/executors/field_extractor_executor.py +13 -12
  102. qtype/interpreter/executors/file_source_executor.py +18 -31
  103. qtype/interpreter/executors/invoke_embedding_executor.py +1 -4
  104. qtype/interpreter/executors/invoke_flow_executor.py +2 -2
  105. qtype/interpreter/executors/invoke_tool_executor.py +19 -18
  106. qtype/interpreter/executors/llm_inference_executor.py +16 -18
  107. qtype/interpreter/executors/prompt_template_executor.py +1 -3
  108. qtype/interpreter/tools/function_tool_helper.py +11 -10
  109. qtype/interpreter/types.py +89 -4
  110. qtype/interpreter/typing.py +31 -32
  111. qtype/mcp/server.py +312 -57
  112. {schema → qtype/schema}/qtype.schema.json +77 -79
  113. qtype/semantic/checker.py +19 -0
  114. qtype/semantic/generate.py +3 -6
  115. qtype/semantic/model.py +26 -33
  116. qtype/semantic/resolver.py +7 -0
  117. qtype/semantic/visualize.py +8 -3
  118. {qtype-0.1.12.dist-info → qtype-0.1.13.dist-info}/METADATA +47 -46
  119. qtype-0.1.13.dist-info/RECORD +352 -0
  120. {qtype-0.1.12.dist-info → qtype-0.1.13.dist-info}/WHEEL +1 -2
  121. docs/How To/Data Processing/read_data_from_files.md +0 -35
  122. docs/components/Aggregate.md +0 -8
  123. docs/components/InvokeFlow.md +0 -8
  124. docs/components/InvokeTool.md +0 -8
  125. docs/components/ToolParameter.md +0 -6
  126. examples/research_assistant/tavily.qtype.yaml +0 -289
  127. qtype/application/facade.py +0 -177
  128. qtype-0.1.12.dist-info/RECORD +0 -325
  129. qtype-0.1.12.dist-info/top_level.txt +0 -1
  130. {docs → qtype/docs}/Contributing/roadmap.md +0 -0
  131. {docs → qtype/docs}/Decisions/ADR-001-Chat-vs-Completion-Endpoint-Features.md +0 -0
  132. {docs → qtype/docs}/Gallery/dataflow_pipelines.mermaid +0 -0
  133. {docs → qtype/docs}/Gallery/research_assistant.mermaid +0 -0
  134. {docs → qtype/docs}/Gallery/simple_chatbot.mermaid +0 -0
  135. {docs → qtype/docs}/How To/Language Features/include_qtype_yaml.md +0 -0
  136. {docs → qtype/docs}/How To/Observability & Debugging/visualize_example.mermaid +0 -0
  137. {docs → qtype/docs}/How To/Qtype Server/flow_as_ui.png +0 -0
  138. {docs → qtype/docs}/Tutorials/example_chat.png +0 -0
  139. {docs → qtype/docs}/Tutorials/index.md +0 -0
  140. {docs → qtype/docs}/components/APIKeyAuthProvider.md +0 -0
  141. {docs → qtype/docs}/components/AWSAuthProvider.md +0 -0
  142. {docs → qtype/docs}/components/AWSSecretManager.md +0 -0
  143. {docs → qtype/docs}/components/Agent.md +0 -0
  144. {docs → qtype/docs}/components/AggregateStats.md +0 -0
  145. {docs → qtype/docs}/components/Application.md +0 -0
  146. {docs → qtype/docs}/components/AuthorizationProvider.md +0 -0
  147. {docs → qtype/docs}/components/AuthorizationProviderList.md +0 -0
  148. {docs → qtype/docs}/components/BearerTokenAuthProvider.md +0 -0
  149. {docs → qtype/docs}/components/BedrockReranker.md +0 -0
  150. {docs → qtype/docs}/components/ChatContent.md +0 -0
  151. {docs → qtype/docs}/components/ChatMessage.md +0 -0
  152. {docs → qtype/docs}/components/ConstantPath.md +0 -0
  153. {docs → qtype/docs}/components/CustomType.md +0 -0
  154. {docs → qtype/docs}/components/Decoder.md +0 -0
  155. {docs → qtype/docs}/components/DecoderFormat.md +0 -0
  156. {docs → qtype/docs}/components/DocToTextConverter.md +0 -0
  157. {docs → qtype/docs}/components/Document.md +0 -0
  158. {docs → qtype/docs}/components/DocumentIndex.md +0 -0
  159. {docs → qtype/docs}/components/DocumentSearch.md +0 -0
  160. {docs → qtype/docs}/components/DocumentSource.md +0 -0
  161. {docs → qtype/docs}/components/Echo.md +0 -0
  162. {docs → qtype/docs}/components/Embedding.md +0 -0
  163. {docs → qtype/docs}/components/EmbeddingModel.md +0 -0
  164. {docs → qtype/docs}/components/FileSource.md +0 -0
  165. {docs → qtype/docs}/components/FileWriter.md +0 -0
  166. {docs → qtype/docs}/components/Flow.md +0 -0
  167. {docs → qtype/docs}/components/FlowInterface.md +0 -0
  168. {docs → qtype/docs}/components/Index.md +0 -0
  169. {docs → qtype/docs}/components/IndexUpsert.md +0 -0
  170. {docs → qtype/docs}/components/InvokeEmbedding.md +0 -0
  171. {docs → qtype/docs}/components/LLMInference.md +0 -0
  172. {docs → qtype/docs}/components/ListType.md +0 -0
  173. {docs → qtype/docs}/components/Memory.md +0 -0
  174. {docs → qtype/docs}/components/MessageRole.md +0 -0
  175. {docs → qtype/docs}/components/Model.md +0 -0
  176. {docs → qtype/docs}/components/ModelList.md +0 -0
  177. {docs → qtype/docs}/components/OAuth2AuthProvider.md +0 -0
  178. {docs → qtype/docs}/components/PromptTemplate.md +0 -0
  179. {docs → qtype/docs}/components/PythonFunctionTool.md +0 -0
  180. {docs → qtype/docs}/components/RAGChunk.md +0 -0
  181. {docs → qtype/docs}/components/RAGDocument.md +0 -0
  182. {docs → qtype/docs}/components/RAGSearchResult.md +0 -0
  183. {docs → qtype/docs}/components/Reranker.md +0 -0
  184. {docs → qtype/docs}/components/SQLSource.md +0 -0
  185. {docs → qtype/docs}/components/Search.md +0 -0
  186. {docs → qtype/docs}/components/SearchResult.md +0 -0
  187. {docs → qtype/docs}/components/SecretManager.md +0 -0
  188. {docs → qtype/docs}/components/SecretReference.md +0 -0
  189. {docs → qtype/docs}/components/TelemetrySink.md +0 -0
  190. {docs → qtype/docs}/components/ToolList.md +0 -0
  191. {docs → qtype/docs}/components/TypeList.md +0 -0
  192. {docs → qtype/docs}/components/VariableList.md +0 -0
  193. {docs → qtype/docs}/components/VectorIndex.md +0 -0
  194. {docs → qtype/docs}/components/VectorSearch.md +0 -0
  195. {docs → qtype/docs}/components/VertexAuthProvider.md +0 -0
  196. {docs → qtype/docs}/components/Writer.md +0 -0
  197. {docs → qtype/docs}/example_ui.png +0 -0
  198. {docs → qtype/docs}/index.md +0 -0
  199. {docs → qtype/docs}/legacy_how_tos/Configuration/modular-yaml.md +0 -0
  200. {docs → qtype/docs}/legacy_how_tos/Configuration/phoenix_projects.png +0 -0
  201. {docs → qtype/docs}/legacy_how_tos/Configuration/phoenix_traces.png +0 -0
  202. {docs → qtype/docs}/legacy_how_tos/Configuration/reference-by-id.md +0 -0
  203. {docs → qtype/docs}/legacy_how_tos/Configuration/telemetry-setup.md +0 -0
  204. {docs → qtype/docs}/legacy_how_tos/Data Types/custom-types.md +0 -0
  205. {docs → qtype/docs}/legacy_how_tos/Data Types/domain-types.md +0 -0
  206. {docs → qtype/docs}/legacy_how_tos/Debugging/visualize-apps.md +0 -0
  207. {docs → qtype/docs}/legacy_how_tos/Tools/api-tools.md +0 -0
  208. {docs → qtype/docs}/legacy_how_tos/Tools/python-tools.md +0 -0
  209. {examples → qtype/examples}/authentication/aws_authentication.qtype.yaml +0 -0
  210. {examples → qtype/examples}/conversational_ai/hello_world_chat.qtype.yaml +0 -0
  211. {examples → qtype/examples}/conversational_ai/simple_chatbot.qtype.yaml +0 -0
  212. {examples → qtype/examples}/data_processing/batch_processing.qtype.yaml +0 -0
  213. {examples → qtype/examples}/data_processing/cache_step_results.qtype.yaml +0 -0
  214. {examples → qtype/examples}/data_processing/collect_results.qtype.yaml +0 -0
  215. {examples → qtype/examples}/data_processing/dataflow_pipelines.qtype.yaml +0 -0
  216. {examples → qtype/examples}/data_processing/decode_json.qtype.yaml +0 -0
  217. {examples → qtype/examples}/data_processing/explode_items.qtype.yaml +0 -0
  218. {examples → qtype/examples}/data_processing/read_file.qtype.yaml +0 -0
  219. {examples → qtype/examples}/invoke_models/create_embeddings.qtype.yaml +0 -0
  220. {examples → qtype/examples}/invoke_models/simple_llm_call.qtype.yaml +0 -0
  221. {examples → qtype/examples}/language_features/include_raw.qtype.yaml +0 -0
  222. {examples → qtype/examples}/language_features/ui_hints.qtype.yaml +0 -0
  223. {examples → qtype/examples}/legacy/bedrock/data_analysis_with_telemetry.qtype.yaml +0 -0
  224. {examples → qtype/examples}/legacy/bedrock/hello_world.qtype.yaml +0 -0
  225. {examples → qtype/examples}/legacy/bedrock/hello_world_chat.qtype.yaml +0 -0
  226. {examples → qtype/examples}/legacy/bedrock/hello_world_chat_with_telemetry.qtype.yaml +0 -0
  227. {examples → qtype/examples}/legacy/bedrock/hello_world_chat_with_thinking.qtype.yaml +0 -0
  228. {examples → qtype/examples}/legacy/bedrock/hello_world_completion.qtype.yaml +0 -0
  229. {examples → qtype/examples}/legacy/bedrock/hello_world_completion_with_auth.qtype.yaml +0 -0
  230. {examples → qtype/examples}/legacy/bedrock/simple_agent_chat.qtype.yaml +0 -0
  231. {examples → qtype/examples}/legacy/chat_with_langfuse.qtype.yaml +0 -0
  232. {examples → qtype/examples}/legacy/data_processor.qtype.yaml +0 -0
  233. {examples → qtype/examples}/legacy/echo/debug_example.qtype.yaml +0 -0
  234. {examples → qtype/examples}/legacy/echo/prompt.qtype.yaml +0 -0
  235. {examples → qtype/examples}/legacy/echo/test.qtype.yaml +0 -0
  236. {examples → qtype/examples}/legacy/echo/video.qtype.yaml +0 -0
  237. {examples → qtype/examples}/legacy/field_extractor_example.qtype.yaml +0 -0
  238. {examples → qtype/examples}/legacy/multi_flow_example.qtype.yaml +0 -0
  239. {examples → qtype/examples}/legacy/openai/hello_world_chat.qtype.yaml +0 -0
  240. {examples → qtype/examples}/legacy/openai/hello_world_chat_with_telemetry.qtype.yaml +0 -0
  241. {examples → qtype/examples}/legacy/rag.qtype.yaml +0 -0
  242. {examples → qtype/examples}/legacy/time_utilities.qtype.yaml +0 -0
  243. {examples → qtype/examples}/legacy/vertex/hello_world_chat.qtype.yaml +0 -0
  244. {examples → qtype/examples}/legacy/vertex/hello_world_completion.qtype.yaml +0 -0
  245. {examples → qtype/examples}/legacy/vertex/hello_world_completion_with_auth.qtype.yaml +0 -0
  246. {examples → qtype/examples}/observability_debugging/trace_with_opentelemetry.qtype.yaml +0 -0
  247. {examples → qtype/examples}/research_assistant/research_assistant.qtype.yaml +0 -0
  248. {examples → qtype/examples}/research_assistant/tavily.oas.yaml +0 -0
  249. {examples → qtype/examples}/tutorials/01_hello_world.qtype.yaml +0 -0
  250. {examples → qtype/examples}/tutorials/02_conversational_chat.qtype.yaml +0 -0
  251. {qtype-0.1.12.dist-info → qtype-0.1.13.dist-info}/entry_points.txt +0 -0
  252. {qtype-0.1.12.dist-info → qtype-0.1.13.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,38 @@
1
1
  from typing import Any, Dict, Literal, Optional, Protocol, Union
2
2
 
3
- from pydantic import BaseModel, ConfigDict, Field
3
+ from pydantic import BaseModel, ConfigDict, Field, model_serializer
4
4
 
5
5
  from qtype.base.types import StrictBaseModel
6
6
  from qtype.dsl.domain_types import ChatMessage
7
7
  from qtype.semantic.model import Step
8
8
 
9
+
10
+ class _UnsetType:
11
+ """Sentinel representing an unset variable.
12
+
13
+ Distinguishes between:
14
+ - Variable never mentioned (not in dict)
15
+ - Variable explicitly unset (UNSET value in dict)
16
+ - Variable set to None (None value in dict)
17
+ """
18
+
19
+ _instance = None
20
+
21
+ def __new__(cls):
22
+ if cls._instance is None:
23
+ cls._instance = super().__new__(cls)
24
+ return cls._instance
25
+
26
+ def __repr__(self) -> str:
27
+ return "UNSET"
28
+
29
+ def __bool__(self) -> bool:
30
+ return False
31
+
32
+
33
+ UNSET = _UnsetType()
34
+
35
+
9
36
  # Stream Event Types (Discriminated Union)
10
37
  # These events are emitted by executors during flow execution
11
38
  # and can be converted to Vercel UI chunks for frontend display
@@ -293,8 +320,9 @@ class FlowMessage(BaseModel):
293
320
  """
294
321
 
295
322
  model_config = ConfigDict(
296
- frozen=True
297
- ) # Enforces immutability at the model level
323
+ frozen=True,
324
+ arbitrary_types_allowed=True, # Allow UNSET sentinel
325
+ )
298
326
 
299
327
  session: Session
300
328
  variables: Dict[str, Any] = Field(
@@ -307,6 +335,49 @@ class FlowMessage(BaseModel):
307
335
  """Checks if this state has encountered an error."""
308
336
  return self.error is not None
309
337
 
338
+ def is_set(self, var_id: str) -> bool:
339
+ """Check if a variable is set (not UNSET, may be None)."""
340
+ value = self.variables.get(var_id, UNSET)
341
+ return value is not UNSET
342
+
343
+ def get_variable(self, var_id: str, *, default: Any = UNSET) -> Any:
344
+ """Get variable value, raising if unset and no default provided.
345
+
346
+ Args:
347
+ var_id: Variable identifier
348
+ default: Value to return if variable is unset. If not provided,
349
+ raises ValueError on unset variables.
350
+
351
+ Returns:
352
+ Variable value (may be None if explicitly set to None)
353
+
354
+ Raises:
355
+ ValueError: If variable is unset and no default provided
356
+
357
+ Examples:
358
+ # Required variable - throws if unset
359
+ value = message.get_variable("user_input")
360
+
361
+ # Optional variable - returns None if unset
362
+ value = message.get_variable("optional_field", default=None)
363
+
364
+ # Optional with custom default
365
+ value = message.get_variable("count", default=0)
366
+ """
367
+ value = self.variables.get(var_id, UNSET)
368
+
369
+ if value is UNSET:
370
+ if default is UNSET:
371
+ raise ValueError(
372
+ (
373
+ f"Required variable '{var_id}' is not set. "
374
+ f"Available variables: {list(self.variables.keys())}"
375
+ )
376
+ )
377
+ return default
378
+
379
+ return value
380
+
310
381
  def copy_with_error(self, step_id: str, exc: Exception) -> "FlowMessage":
311
382
  """Returns a copy of this state marked as failed."""
312
383
  return self.model_copy(
@@ -319,15 +390,29 @@ class FlowMessage(BaseModel):
319
390
  }
320
391
  )
321
392
 
322
- # It's useful to have copy-on-write style helpers
323
393
  def copy_with_variables(
324
394
  self, new_variables: dict[str, Any]
325
395
  ) -> "FlowMessage":
396
+ """Create a new FlowMessage with updated variables.
397
+
398
+ Note: Can set variables to UNSET to explicitly mark them as unset.
399
+ """
326
400
  new_vars = self.variables.copy()
327
401
  new_vars.update(new_variables)
328
402
  new_state = self.model_copy(update={"variables": new_vars})
329
403
  return new_state
330
404
 
405
+ @model_serializer
406
+ def serialize_model(self):
407
+ """Custom serialization that excludes UNSET variables."""
408
+ return {
409
+ "session": self.session,
410
+ "variables": {
411
+ k: v for k, v in self.variables.items() if v is not UNSET
412
+ },
413
+ "error": self.error,
414
+ }
415
+
331
416
 
332
417
  class InterpreterError(Exception):
333
418
  """Base exception class for ProtoGen interpreter errors."""
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import uuid
4
- from typing import Any, Type, get_origin
4
+ from typing import Any, Type
5
5
 
6
- from pydantic import BaseModel, Field, TypeAdapter, create_model
6
+ from pydantic import BaseModel, Field, create_model
7
7
 
8
8
  from qtype.dsl.model import ListType, PrimitiveTypeEnum
9
9
  from qtype.dsl.model import Variable as DSLVariable
@@ -138,37 +138,36 @@ def flow_results_to_output_container(
138
138
  return output_container(outputs=outputs, errors=errors)
139
139
 
140
140
 
141
- def instantiate_variable(variable: DSLVariable, value: Any) -> Any:
141
+ def convert_dict_to_typed_variables(
142
+ data: dict[str, Any], variables: list[Variable]
143
+ ) -> dict[str, Any]:
142
144
  """
143
- Unified contract to ensure data matches its QType definition.
144
- Handles CustomTypes, DomainTypes, and Primitives.
145
+ Convert a dictionary of raw values to properly typed variables.
146
+
147
+ Uses Pydantic model validation to convert all values at once based on
148
+ Variable type declarations. This is more efficient than converting each
149
+ field individually.
150
+
151
+ Args:
152
+ data: Dictionary with raw values (e.g., from DataFrame row)
153
+ variables: List of Variable definitions with type information
154
+
155
+ Returns:
156
+ Dictionary with values converted to their declared types
157
+
158
+ Raises:
159
+ ValidationError: If values cannot be converted to declared types
145
160
  """
146
- target_type, _ = _get_variable_type(variable)
161
+ # Create a Pydantic model from the variable definitions
162
+ model_class = create_model(
163
+ "TypedVariables",
164
+ __base__=BaseModel,
165
+ **_fields_from_variables(variables),
166
+ )
147
167
 
148
- # 1. Handle the 'Parameterized Generic' Check (The isinstance fix)
149
- # We check if target_type is a generic (like list[T]) vs a simple class.
150
- origin = get_origin(target_type)
168
+ # Validate and convert the data using Pydantic
169
+ validated = model_class.model_validate(data)
151
170
 
152
- if origin is None:
153
- # It's a simple type (int, RAGChunk, etc.)
154
- if isinstance(value, target_type):
155
- return value
156
- else:
157
- # It's a generic (list[str], etc.).
158
- # We skip the identity check and let TypeAdapter handle it.
159
- pass
160
-
161
- # 2. Handle Pydantic Models (Custom/Domain Types)
162
- if hasattr(target_type, "model_validate"):
163
- return target_type.model_validate(value) # type: ignore[misc]
164
-
165
- # 3. Handle Primitives & Complex Python Types (List, Optional, Union)
166
- try:
167
- # TypeAdapter is the "V2 way" to validate things that aren't
168
- # full Pydantic models (like List[int] or Optional[str])
169
- return TypeAdapter(target_type).validate_python(value)
170
- except Exception:
171
- # Fallback to your original manual cast if TypeAdapter is overkill
172
- if isinstance(target_type, type):
173
- return target_type(value)
174
- raise ValueError(f"Unsupported target type: {target_type}")
171
+ # Return as dict but preserve actual typed instances (not serialized)
172
+ # Use __dict__ to get the actual field values without serialization
173
+ return dict(validated)
qtype/mcp/server.py CHANGED
@@ -2,11 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import re
5
+ import tempfile
5
6
  from functools import lru_cache
6
7
  from importlib.resources import files
7
8
  from pathlib import Path
8
9
  from typing import Any
9
10
 
11
+ import tantivy
10
12
  from mcp.server.fastmcp import FastMCP
11
13
  from pydantic import BaseModel
12
14
 
@@ -20,26 +22,128 @@ SNIPPET_REGEX = re.compile(r'--8<--\s+"([^"]+)"')
20
22
 
21
23
 
22
24
  # ============================================================================
23
- # Helper Functions
25
+ # Resource Abstraction Layer
24
26
  # ============================================================================
25
27
 
26
28
 
27
- def _get_docs_path() -> Path:
28
- """Get the path to the documentation directory.
29
+ class ResourceDirectory:
30
+ """Abstraction for accessing resource directories (docs, examples, etc.)."""
31
+
32
+ def __init__(
33
+ self, name: str, file_extension: str, resolve_snippets: bool = False
34
+ ):
35
+ """Initialize a resource directory.
36
+
37
+ Args:
38
+ name: Directory name (e.g., "docs", "examples")
39
+ file_extension: File extension to search for (e.g., ".md", ".yaml")
40
+ resolve_snippets: Whether to resolve MkDocs snippets in file content
41
+ """
42
+ self.name = name
43
+ self.file_extension = file_extension
44
+ self.resolve_snippets = resolve_snippets
45
+ self._path_cache: Path | None = None
46
+
47
+ def get_path(self) -> Path:
48
+ """Get the path to this resource directory.
49
+
50
+ Returns:
51
+ Path to the resource directory, trying installed package first,
52
+ then falling back to development path.
53
+ """
54
+ if self._path_cache is not None:
55
+ return self._path_cache
56
+
57
+ try:
58
+ # Try to get from installed package
59
+ resource_root = files("qtype") / self.name
60
+ # Check if it exists by trying to iterate
61
+ list(resource_root.iterdir())
62
+ self._path_cache = Path(str(resource_root))
63
+ except (FileNotFoundError, AttributeError, TypeError):
64
+ # Fall back to development path
65
+ self._path_cache = Path(__file__).parent.parent.parent / self.name
66
+
67
+ return self._path_cache
68
+
69
+ def get_file(self, file_path: str) -> str:
70
+ """Get the content of a specific file.
71
+
72
+ Args:
73
+ file_path: Relative path to the file from the resource root.
74
+
75
+ Returns:
76
+ The full content of the file.
77
+
78
+ Raises:
79
+ FileNotFoundError: If the specified file doesn't exist.
80
+ ValueError: If the path tries to access files outside the directory.
81
+ """
82
+ resource_path = self.get_path()
83
+
84
+ # Resolve the requested file path
85
+ requested_file = (resource_path / file_path).resolve()
86
+
87
+ # Security check: ensure the resolved path is within resource directory
88
+ try:
89
+ requested_file.relative_to(resource_path.resolve())
90
+ except ValueError:
91
+ raise ValueError(
92
+ f"Invalid path: '{file_path}' is outside {self.name} directory"
93
+ )
94
+
95
+ if not requested_file.exists():
96
+ raise FileNotFoundError(
97
+ f"{self.name.capitalize()} file not found: '{file_path}'. "
98
+ f"Use list_{self.name} to see available files."
99
+ )
100
+
101
+ if not requested_file.is_file():
102
+ raise ValueError(f"Path is not a file: '{file_path}'")
103
+
104
+ content = requested_file.read_text(encoding="utf-8")
105
+
106
+ # Apply snippet resolution if enabled
107
+ if self.resolve_snippets:
108
+ content = _resolve_snippets(content, requested_file)
109
+
110
+ return content
111
+
112
+ def list_files(self) -> list[str]:
113
+ """List all files in this resource directory.
114
+
115
+ Returns:
116
+ Sorted list of relative paths to all files with the configured extension.
117
+
118
+ Raises:
119
+ FileNotFoundError: If the resource directory doesn't exist.
120
+ """
121
+ resource_path = self.get_path()
122
+
123
+ if not resource_path.exists():
124
+ raise FileNotFoundError(
125
+ f"{self.name.capitalize()} directory not found: {resource_path}"
126
+ )
127
+
128
+ # Find all files with the configured extension
129
+ pattern = f"*{self.file_extension}"
130
+ files_list = []
131
+ for file in resource_path.rglob(pattern):
132
+ # Get relative path from resource root
133
+ rel_path = file.relative_to(resource_path)
134
+ files_list.append(str(rel_path))
135
+
136
+ return sorted(files_list)
137
+
138
+
139
+ # Initialize resource directories
140
+ _docs_resource = ResourceDirectory("docs", ".md", resolve_snippets=True)
141
+ _examples_resource = ResourceDirectory("examples", ".yaml")
29
142
 
30
- Returns:
31
- Path to the docs directory, trying installed package first,
32
- then falling back to development path.
33
- """
34
- try:
35
- # Try to get from installed package
36
- docs_root = files("qtype") / "docs"
37
- # Check if it exists by trying to iterate
38
- list(docs_root.iterdir())
39
- return Path(str(docs_root))
40
- except (FileNotFoundError, AttributeError, TypeError):
41
- # Fall back to development path
42
- return Path(__file__).parent.parent.parent / "docs"
143
+
144
+ # ============================================================================
145
+ # Helper Functions
146
+ # ============================================================================
43
147
 
44
148
 
45
149
  @lru_cache(maxsize=1)
@@ -55,7 +159,7 @@ def _load_schema() -> dict[str, Any]:
55
159
  """
56
160
  # Try to load from installed package data first
57
161
  try:
58
- schema_file = files("qtype") / "qtype.schema.json"
162
+ schema_file = files("qtype") / "schema" / "qtype.schema.json"
59
163
  schema_text = schema_file.read_text(encoding="utf-8")
60
164
  return json.loads(schema_text)
61
165
  except (FileNotFoundError, AttributeError):
@@ -67,7 +171,7 @@ def _load_schema() -> dict[str, Any]:
67
171
  return json.load(f)
68
172
 
69
173
 
70
- def resolve_snippets(content: str, base_path: Path) -> str:
174
+ def _resolve_snippets(content: str, base_path: Path) -> str:
71
175
  """
72
176
  Recursively finds and replaces MkDocs snippets in markdown content.
73
177
  Mimics the behavior of pymdownx.snippets.
@@ -76,7 +180,7 @@ def resolve_snippets(content: str, base_path: Path) -> str:
76
180
  content: The markdown content to process
77
181
  base_path: Path to the file being processed (used to resolve relative paths)
78
182
  """
79
- docs_root = _get_docs_path()
183
+ docs_root = _docs_resource.get_path()
80
184
  project_root = docs_root.parent
81
185
 
82
186
  def replace_match(match):
@@ -92,7 +196,7 @@ def resolve_snippets(content: str, base_path: Path) -> str:
92
196
  for candidate in candidates:
93
197
  if candidate.exists() and candidate.is_file():
94
198
  # Recursively resolve snippets inside the included file
95
- return resolve_snippets(
199
+ return _resolve_snippets(
96
200
  candidate.read_text(encoding="utf-8"), candidate
97
201
  )
98
202
 
@@ -101,6 +205,84 @@ def resolve_snippets(content: str, base_path: Path) -> str:
101
205
  return SNIPPET_REGEX.sub(replace_match, content)
102
206
 
103
207
 
208
+ @lru_cache(maxsize=1)
209
+ def _build_search_index() -> tantivy.Index:
210
+ """Build and cache a Tantivy search index for docs and examples.
211
+
212
+ Returns:
213
+ A Tantivy Index containing all documentation markdown files
214
+ and example YAML files.
215
+
216
+ Raises:
217
+ Exception: If index building fails.
218
+ """
219
+ docs_path = _docs_resource.get_path()
220
+ examples_path = _examples_resource.get_path()
221
+
222
+ # Create schema with fields for title, path, and content
223
+ schema_builder = tantivy.SchemaBuilder()
224
+ schema_builder.add_text_field("title", stored=True)
225
+ schema_builder.add_text_field("path", stored=True)
226
+ schema_builder.add_text_field("content", stored=True)
227
+ schema_builder.add_text_field("type", stored=True)
228
+ schema = schema_builder.build()
229
+
230
+ # Create index in temporary directory
231
+ index = tantivy.Index(schema, path=tempfile.mkdtemp())
232
+ writer = index.writer()
233
+
234
+ # Helper to index files
235
+ def index_files(
236
+ root_path: Path,
237
+ pattern: str,
238
+ type_label: str,
239
+ path_prefix: str,
240
+ process_content=None,
241
+ extract_title=None,
242
+ ):
243
+ for file_path in root_path.rglob(pattern):
244
+ content = file_path.read_text(encoding="utf-8")
245
+ if process_content:
246
+ content = process_content(content, file_path)
247
+
248
+ rel_path = str(file_path.relative_to(root_path))
249
+ title = (
250
+ extract_title(content, file_path)
251
+ if extract_title
252
+ else file_path.stem
253
+ )
254
+
255
+ writer.add_document(
256
+ tantivy.Document(
257
+ title=title,
258
+ path=f"{path_prefix}/{rel_path}",
259
+ content=content,
260
+ type=type_label,
261
+ )
262
+ )
263
+
264
+ # Extract title from markdown first heading
265
+ def extract_md_title(content: str, file_path: Path) -> str:
266
+ for line in content.split("\n"):
267
+ if line.startswith("# "):
268
+ return line[2:].strip()
269
+ return file_path.stem
270
+
271
+ # Index documentation and examples
272
+ index_files(
273
+ docs_path,
274
+ "*.md",
275
+ "documentation",
276
+ "docs",
277
+ process_content=_resolve_snippets,
278
+ extract_title=extract_md_title,
279
+ )
280
+ index_files(examples_path, "*.yaml", "example", "examples")
281
+
282
+ writer.commit()
283
+ return index
284
+
285
+
104
286
  # ============================================================================
105
287
  # Tool Functions
106
288
  # ============================================================================
@@ -122,6 +304,10 @@ class MermaidVisualizationResult(BaseModel):
122
304
  preview_instructions: str
123
305
 
124
306
 
307
+ # Rebuild model after nested dependency is defined
308
+ MermaidVisualizationResult.model_rebuild()
309
+
310
+
125
311
  @mcp.tool(
126
312
  title="Convert API Specification to QType Tools",
127
313
  description=(
@@ -287,36 +473,13 @@ def get_documentation(file_path: str) -> str:
287
473
  Use list_documentation to see all available files.
288
474
 
289
475
  Returns:
290
- The full markdown content of the documentation file.
476
+ The full markdown content of the documentation file with snippets resolved.
291
477
 
292
478
  Raises:
293
479
  FileNotFoundError: If the specified file doesn't exist.
294
480
  ValueError: If the path tries to access files outside the docs directory.
295
481
  """
296
- docs_path = _get_docs_path()
297
-
298
- # Resolve the requested file path
299
- requested_file = (docs_path / file_path).resolve()
300
-
301
- # Security check: ensure the resolved path is within docs directory
302
- try:
303
- requested_file.relative_to(docs_path.resolve())
304
- except ValueError:
305
- raise ValueError(
306
- f"Invalid path: '{file_path}' is outside documentation directory"
307
- )
308
-
309
- if not requested_file.exists():
310
- raise FileNotFoundError(
311
- f"Documentation file not found: '{file_path}'. "
312
- "Use list_documentation to see available files."
313
- )
314
-
315
- if not requested_file.is_file():
316
- raise ValueError(f"Path is not a file: '{file_path}'")
317
-
318
- content = requested_file.read_text(encoding="utf-8")
319
- return resolve_snippets(content, requested_file)
482
+ return _docs_resource.get_file(file_path)
320
483
 
321
484
 
322
485
  @mcp.tool(
@@ -363,21 +526,113 @@ def list_documentation() -> list[str]:
363
526
  Paths are relative to the docs root (e.g., "components/Flow.md",
364
527
  "Tutorials/getting_started.md").
365
528
  """
366
- docs_path = _get_docs_path()
529
+ return _docs_resource.list_files()
367
530
 
368
- if not docs_path.exists():
369
- raise FileNotFoundError(
370
- f"Documentation directory not found: {docs_path}"
371
- )
372
531
 
373
- # Find all markdown files
374
- md_files = []
375
- for md_file in docs_path.rglob("*.md"):
376
- # Get relative path from docs root
377
- rel_path = md_file.relative_to(docs_path)
378
- md_files.append(str(rel_path))
532
+ @mcp.tool(
533
+ title="Get QType Example",
534
+ description=(
535
+ "Returns the content of a specific example YAML file. "
536
+ "Use list_examples first to see available files. "
537
+ "Provide the relative path (e.g., 'conversational_ai/simple_chatbot.qtype.yaml', "
538
+ "'data_processing/csv_processor.qtype.yaml')."
539
+ ),
540
+ )
541
+ def get_example(file_path: str) -> str:
542
+ """Get the content of a specific example file.
543
+
544
+ Args:
545
+ file_path: Relative path to the example file from the examples root.
546
+ Example: "conversational_ai/simple_chatbot.qtype.yaml",
547
+ "data_processing/csv_processor.qtype.yaml".
548
+ Use list_examples to see all available files.
549
+
550
+ Returns:
551
+ The full YAML content of the example file.
552
+
553
+ Raises:
554
+ FileNotFoundError: If the specified file doesn't exist.
555
+ ValueError: If the path tries to access files outside the examples directory.
556
+ """
557
+ return _examples_resource.get_file(file_path)
558
+
559
+
560
+ @mcp.tool(
561
+ title="List QType Examples",
562
+ description=(
563
+ "Returns a list of all available example YAML files. "
564
+ "Use this to discover what examples exist, then retrieve "
565
+ "specific files with get_example. Examples are organized by category: "
566
+ "conversational_ai/ (chatbots and agents), data_processing/ (ETL and transformations), "
567
+ "invoke_models/ (LLM usage), language_features/ (QType syntax examples), etc."
568
+ ),
569
+ structured_output=True,
570
+ )
571
+ def list_examples() -> list[str]:
572
+ """List all available example YAML files.
573
+
574
+ Returns:
575
+ Sorted list of relative paths to all .yaml example files.
576
+ Paths are relative to the examples root (e.g.,
577
+ "conversational_ai/simple_chatbot.qtype.yaml",
578
+ "data_processing/csv_processor.qtype.yaml").
579
+ """
580
+ return _examples_resource.list_files()
581
+
582
+
583
+ @mcp.tool(
584
+ title="Search QType Library",
585
+ description=(
586
+ "Full-text search across all QType documentation and examples. "
587
+ "Returns matching documents and example YAML files ranked by relevance. "
588
+ "Use this to find documentation about specific topics, features, or components, "
589
+ "or to discover example implementations. Doc paths can be used with get_documentation."
590
+ ),
591
+ structured_output=True,
592
+ )
593
+ def search_library(query: str, limit: int = 10) -> list[dict[str, Any]]:
594
+ """Search library using full-text search.
595
+
596
+ Args:
597
+ query: Search query string. Can include multiple words, phrases,
598
+ or boolean operators (AND, OR, NOT).
599
+ limit: Maximum number of results to return (default: 10, max: 50).
600
+
601
+ Returns:
602
+ List of matching items with:
603
+ - title: Item title
604
+ - path: Relative path (docs/ or examples/ prefix)
605
+ - type: Either "documentation" or "example"
606
+ - score: Relevance score (higher is more relevant)
607
+
608
+ Examples:
609
+ search_library("flow execution") # Find docs/examples about flows
610
+ search_library("DocumentSource") # Find component docs
611
+ search_library("authentication AND API") # Boolean search
612
+ """
613
+ # Clamp limit to reasonable range
614
+ limit = max(1, min(limit, 50))
615
+
616
+ index = _build_search_index()
617
+ index.reload()
618
+ searcher = index.searcher()
619
+ tantivy_query = index.parse_query(query, ["title", "content"])
620
+
621
+ search_results = searcher.search(tantivy_query, limit)
622
+
623
+ results = []
624
+ for score, doc_address in search_results.hits:
625
+ doc = searcher.doc(doc_address)
626
+ results.append(
627
+ {
628
+ "title": doc["title"][0],
629
+ "path": doc["path"][0],
630
+ "type": doc["type"][0],
631
+ "score": score,
632
+ }
633
+ )
379
634
 
380
- return sorted(md_files)
635
+ return results
381
636
 
382
637
 
383
638
  @mcp.tool(