qtype 0.0.1__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/__init__.py +0 -0
- qtype/cli.py +73 -0
- qtype/commands/__init__.py +5 -0
- qtype/commands/convert.py +76 -0
- qtype/commands/generate.py +107 -0
- qtype/commands/run.py +200 -0
- qtype/commands/validate.py +83 -0
- qtype/commons/__init__.py +0 -0
- qtype/commons/generate.py +88 -0
- qtype/commons/tools.py +192 -0
- qtype/converters/__init__.py +0 -0
- qtype/converters/tools_from_api.py +24 -0
- qtype/converters/tools_from_module.py +326 -0
- qtype/converters/types.py +20 -0
- qtype/dsl/__init__.py +1 -0
- qtype/dsl/base_types.py +31 -0
- qtype/dsl/document.py +108 -0
- qtype/dsl/domain_types.py +56 -0
- qtype/dsl/model.py +685 -0
- qtype/dsl/validator.py +439 -0
- qtype/interpreter/__init__.py +1 -0
- qtype/interpreter/api.py +104 -0
- qtype/interpreter/conversions.py +148 -0
- qtype/interpreter/exceptions.py +10 -0
- qtype/interpreter/flow.py +37 -0
- qtype/interpreter/resource_cache.py +37 -0
- qtype/interpreter/step.py +67 -0
- qtype/interpreter/steps/__init__.py +0 -0
- qtype/interpreter/steps/agent.py +114 -0
- qtype/interpreter/steps/condition.py +36 -0
- qtype/interpreter/steps/decoder.py +84 -0
- qtype/interpreter/steps/llm_inference.py +127 -0
- qtype/interpreter/steps/prompt_template.py +54 -0
- qtype/interpreter/steps/search.py +24 -0
- qtype/interpreter/steps/tool.py +53 -0
- qtype/interpreter/telemetry.py +16 -0
- qtype/interpreter/typing.py +78 -0
- qtype/loader.py +341 -0
- qtype/semantic/__init__.py +0 -0
- qtype/semantic/errors.py +4 -0
- qtype/semantic/generate.py +383 -0
- qtype/semantic/model.py +354 -0
- qtype/semantic/resolver.py +97 -0
- qtype-0.0.1.dist-info/METADATA +120 -0
- qtype-0.0.1.dist-info/RECORD +49 -0
- qtype-0.0.1.dist-info/WHEEL +5 -0
- qtype-0.0.1.dist-info/entry_points.txt +2 -0
- qtype-0.0.1.dist-info/licenses/LICENSE +202 -0
- qtype-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class InterpreterError(Exception):
|
|
5
|
+
"""Base exception class for ProtoGen interpreter errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, details: Any = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.message = message
|
|
10
|
+
self.details = details
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from qtype.interpreter.exceptions import InterpreterError
|
|
7
|
+
from qtype.interpreter.step import execute_step
|
|
8
|
+
from qtype.semantic.model import Flow, Variable
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute_flow(flow: Flow, **kwargs: dict[Any, Any]) -> list[Variable]:
|
|
14
|
+
"""Execute a flow based on the provided arguments.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
flow: The flow to execute.
|
|
18
|
+
inputs: The input variables for the flow.
|
|
19
|
+
**kwargs: Additional keyword arguments.
|
|
20
|
+
"""
|
|
21
|
+
logger.debug(f"Executing step: {flow.id} with kwargs: {kwargs}")
|
|
22
|
+
|
|
23
|
+
unset_inputs = [input for input in flow.inputs if not input.is_set()]
|
|
24
|
+
if unset_inputs:
|
|
25
|
+
raise InterpreterError(
|
|
26
|
+
f"The following inputs are required but have no values: {', '.join([input.id for input in unset_inputs])}"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for step in flow.steps:
|
|
30
|
+
execute_step(step, **kwargs)
|
|
31
|
+
|
|
32
|
+
unset_outputs = [output for output in flow.outputs if not output.is_set()]
|
|
33
|
+
if unset_outputs:
|
|
34
|
+
raise InterpreterError(
|
|
35
|
+
f"The following outputs are required but have no values: {', '.join([output.id for output in unset_outputs])}"
|
|
36
|
+
)
|
|
37
|
+
return flow.outputs
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from cachetools import LRUCache
|
|
6
|
+
|
|
7
|
+
# Global LRU cache with a reasonable default size
|
|
8
|
+
_RESOURCE_CACHE_MAX_SIZE = int(os.environ.get("RESOURCE_CACHE_MAX_SIZE", 128))
|
|
9
|
+
_GLOBAL_RESOURCE_CACHE = LRUCache(maxsize=_RESOURCE_CACHE_MAX_SIZE)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def cached_resource(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
13
|
+
"""
|
|
14
|
+
Decorator to cache function results using a global LRU cache.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
func: The function to cache.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
The wrapped function with caching.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@functools.wraps(func)
|
|
24
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
25
|
+
cache_key = (
|
|
26
|
+
func.__module__,
|
|
27
|
+
func.__qualname__,
|
|
28
|
+
args,
|
|
29
|
+
tuple(sorted(kwargs.items())),
|
|
30
|
+
)
|
|
31
|
+
if cache_key in _GLOBAL_RESOURCE_CACHE:
|
|
32
|
+
return _GLOBAL_RESOURCE_CACHE[cache_key]
|
|
33
|
+
result = func(*args, **kwargs)
|
|
34
|
+
_GLOBAL_RESOURCE_CACHE[cache_key] = result
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
return wrapper
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from qtype.interpreter.exceptions import InterpreterError
|
|
7
|
+
from qtype.interpreter.steps import (
|
|
8
|
+
agent,
|
|
9
|
+
condition,
|
|
10
|
+
decoder,
|
|
11
|
+
llm_inference,
|
|
12
|
+
prompt_template,
|
|
13
|
+
search,
|
|
14
|
+
tool,
|
|
15
|
+
)
|
|
16
|
+
from qtype.semantic.model import (
|
|
17
|
+
Agent,
|
|
18
|
+
Condition,
|
|
19
|
+
Decoder,
|
|
20
|
+
Flow,
|
|
21
|
+
LLMInference,
|
|
22
|
+
PromptTemplate,
|
|
23
|
+
Search,
|
|
24
|
+
Step,
|
|
25
|
+
Tool,
|
|
26
|
+
Variable,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def execute_step(step: Step, **kwargs: dict[str, Any]) -> list[Variable]:
|
|
33
|
+
"""Execute a single step within a flow.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
step: The step to execute.
|
|
37
|
+
**kwargs: Additional keyword arguments.
|
|
38
|
+
"""
|
|
39
|
+
logger.debug(f"Executing step: {step.id} with kwargs: {kwargs}")
|
|
40
|
+
|
|
41
|
+
unset_inputs = [input for input in step.inputs if not input.is_set()]
|
|
42
|
+
if unset_inputs:
|
|
43
|
+
raise InterpreterError(
|
|
44
|
+
f"The following inputs are required but have no values: {', '.join([input.id for input in unset_inputs])}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if isinstance(step, Agent):
|
|
48
|
+
return agent.execute(step=step, **kwargs) # type: ignore[arg-type]
|
|
49
|
+
elif isinstance(step, Condition):
|
|
50
|
+
return condition.execute(condition=step, **kwargs)
|
|
51
|
+
elif isinstance(step, Decoder):
|
|
52
|
+
return decoder.execute(step=step, **kwargs) # type: ignore[arg-type]
|
|
53
|
+
elif isinstance(step, Flow):
|
|
54
|
+
from .flow import execute_flow
|
|
55
|
+
|
|
56
|
+
return execute_flow(step, **kwargs) # type: ignore[arg-type]
|
|
57
|
+
elif isinstance(step, LLMInference):
|
|
58
|
+
return llm_inference.execute(step, **kwargs) # type: ignore[arg-type]
|
|
59
|
+
elif isinstance(step, PromptTemplate):
|
|
60
|
+
return prompt_template.execute(step, **kwargs) # type: ignore[arg-type]
|
|
61
|
+
elif isinstance(step, Search):
|
|
62
|
+
return search.execute(step, **kwargs) # type: ignore[arg-type]
|
|
63
|
+
elif isinstance(step, Tool):
|
|
64
|
+
return tool.execute(step, **kwargs) # type: ignore[arg-type]
|
|
65
|
+
else:
|
|
66
|
+
# Handle other step types if necessary
|
|
67
|
+
raise InterpreterError(f"Unsupported step type: {type(step).__name__}")
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import importlib
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from llama_index.core.agent.workflow import ReActAgent
|
|
7
|
+
from llama_index.core.base.llms.types import ChatMessage as LlamaChatMessage
|
|
8
|
+
from llama_index.core.tools import AsyncBaseTool, FunctionTool
|
|
9
|
+
from llama_index.core.workflow import Context
|
|
10
|
+
from llama_index.core.workflow.handler import WorkflowHandler # type: ignore
|
|
11
|
+
|
|
12
|
+
from qtype.dsl.domain_types import ChatMessage
|
|
13
|
+
from qtype.interpreter.conversions import (
|
|
14
|
+
from_chat_message,
|
|
15
|
+
to_chat_message,
|
|
16
|
+
to_llm,
|
|
17
|
+
to_memory,
|
|
18
|
+
)
|
|
19
|
+
from qtype.interpreter.exceptions import InterpreterError
|
|
20
|
+
from qtype.semantic.model import Agent, APITool, PythonFunctionTool, Variable
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def to_llama_tool(tool: PythonFunctionTool) -> AsyncBaseTool:
|
|
26
|
+
"""Convert a qtype Tool to a LlamaIndex Tool."""
|
|
27
|
+
# We want to get the function named by the tool -- get ".tools.<tool_name>"
|
|
28
|
+
# This assumes the tool name matches a function in the .tools module
|
|
29
|
+
module = importlib.import_module(tool.module_path)
|
|
30
|
+
function = getattr(module, tool.function_name, None)
|
|
31
|
+
if function is None:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"Tool function '{tool.function_name}' not found in module '{tool.module_path}'."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
return FunctionTool.from_defaults(
|
|
37
|
+
fn=function, name=tool.name, description=tool.description
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def execute(agent: Agent, **kwargs: dict[str, Any]) -> list[Variable]:
|
|
42
|
+
"""Execute an agent step.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
agent: The agent step to execute.
|
|
46
|
+
**kwargs: Additional keyword arguments.
|
|
47
|
+
"""
|
|
48
|
+
logger.debug(f"Executing agent step: {agent.id}")
|
|
49
|
+
if len(agent.outputs) != 1:
|
|
50
|
+
raise InterpreterError(
|
|
51
|
+
"LLMInference step must have exactly one output variable."
|
|
52
|
+
)
|
|
53
|
+
output_variable = agent.outputs[0]
|
|
54
|
+
|
|
55
|
+
# prepare the input for the agent
|
|
56
|
+
if len(agent.inputs) != 1:
|
|
57
|
+
# TODO: Support multiple inputs by shoving it into the chat history?
|
|
58
|
+
raise InterpreterError(
|
|
59
|
+
"Agent step must have exactly one input variable."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
input_variable = agent.inputs[0]
|
|
63
|
+
if input_variable.type == ChatMessage:
|
|
64
|
+
input: LlamaChatMessage | str = to_chat_message(input_variable.value) # type: ignore
|
|
65
|
+
else:
|
|
66
|
+
input: LlamaChatMessage | str = input_variable.value # type: ignore
|
|
67
|
+
|
|
68
|
+
# Pepare the tools
|
|
69
|
+
# TODO: support api tools
|
|
70
|
+
if any(isinstance(tool, APITool) for tool in agent.tools):
|
|
71
|
+
raise NotImplementedError(
|
|
72
|
+
"APITool is not supported in the current implementation. Please use PythonFunctionTool."
|
|
73
|
+
)
|
|
74
|
+
tools = [
|
|
75
|
+
to_llama_tool(tool) # type: ignore
|
|
76
|
+
for tool in (agent.tools if agent.tools else [])
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# prep memory
|
|
80
|
+
# Note to_memory is a cached resource so this will get existing memory if available
|
|
81
|
+
memory = (
|
|
82
|
+
to_memory(kwargs.get("session_id"), agent.memory)
|
|
83
|
+
if agent.memory
|
|
84
|
+
else None
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Run the agent
|
|
88
|
+
async def run_agent() -> WorkflowHandler:
|
|
89
|
+
logger.debug(
|
|
90
|
+
f"Starting agent '{agent.id}' execution with input length: {len(str(input))} (ReAct mode)"
|
|
91
|
+
)
|
|
92
|
+
re_agent = ReActAgent(
|
|
93
|
+
name=agent.id,
|
|
94
|
+
tools=tools, # type: ignore
|
|
95
|
+
system_prompt=agent.system_message,
|
|
96
|
+
llm=to_llm(agent.model, agent.system_message), # type: ignore
|
|
97
|
+
)
|
|
98
|
+
ctx = Context(re_agent) # type: ignore
|
|
99
|
+
# TODO: implement checkpoint_callback to call stream_fn?
|
|
100
|
+
handler = re_agent.run(input, chat_memory=memory, ctx=ctx)
|
|
101
|
+
result = await handler
|
|
102
|
+
logger.debug(
|
|
103
|
+
f"Agent '{agent.id}' execution completed successfully (ReAct mode)"
|
|
104
|
+
)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
result = asyncio.run(run_agent())
|
|
108
|
+
|
|
109
|
+
if output_variable.type == ChatMessage:
|
|
110
|
+
output_variable.value = from_chat_message(result.response) # type: ignore
|
|
111
|
+
else:
|
|
112
|
+
output_variable.value = result.response.content # type: ignore
|
|
113
|
+
|
|
114
|
+
return agent.outputs
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from qtype.semantic.model import Condition, Variable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def execute(condition: Condition, **kwargs: dict[str, Any]) -> list[Variable]:
|
|
9
|
+
"""Execute a condition step.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
condition: The condition step to execute.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
A list of variables that are set based on the condition evaluation.
|
|
16
|
+
"""
|
|
17
|
+
from qtype.interpreter.step import execute_step
|
|
18
|
+
|
|
19
|
+
if not condition.inputs:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"Condition step requires at least one input variable."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if len(condition.inputs) != 1:
|
|
25
|
+
raise ValueError(
|
|
26
|
+
f"Condition step {condition.id} must have exactly one input, found {len(condition.inputs)}."
|
|
27
|
+
)
|
|
28
|
+
input_var = condition.inputs[0]
|
|
29
|
+
if condition.equals.value == input_var.value: # type: ignore
|
|
30
|
+
# If the condition is met, return the outputs
|
|
31
|
+
return execute_step(condition.then, **kwargs)
|
|
32
|
+
elif condition.else_:
|
|
33
|
+
return execute_step(condition.else_, **kwargs)
|
|
34
|
+
else:
|
|
35
|
+
# If no else branch is defined, return an empty list
|
|
36
|
+
return []
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import xml.etree.ElementTree as ET
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from qtype.dsl.model import DecoderFormat
|
|
6
|
+
from qtype.semantic.model import Decoder, Variable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_json(input: str) -> dict[str, Any]:
|
|
10
|
+
"""Parse a JSON string into a Python object."""
|
|
11
|
+
try:
|
|
12
|
+
cleaned_response = input.strip()
|
|
13
|
+
if cleaned_response.startswith("```json"):
|
|
14
|
+
cleaned_response = cleaned_response[7:]
|
|
15
|
+
if cleaned_response.endswith("```"):
|
|
16
|
+
cleaned_response = cleaned_response[:-3]
|
|
17
|
+
cleaned_response = cleaned_response.strip()
|
|
18
|
+
|
|
19
|
+
# Parse the JSON
|
|
20
|
+
parsed = json.loads(cleaned_response)
|
|
21
|
+
if not isinstance(parsed, dict):
|
|
22
|
+
raise ValueError(f"Parsed JSON is not an object: {parsed}")
|
|
23
|
+
return parsed
|
|
24
|
+
except json.JSONDecodeError as e:
|
|
25
|
+
raise ValueError(f"Invalid JSON input: {e}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_xml(input: str) -> dict[str, Any]:
|
|
29
|
+
"""Parse an XML string into a Python object."""
|
|
30
|
+
try:
|
|
31
|
+
cleaned_response = input.strip()
|
|
32
|
+
if cleaned_response.startswith("```xml"):
|
|
33
|
+
cleaned_response = cleaned_response[6:]
|
|
34
|
+
if cleaned_response.endswith("```"):
|
|
35
|
+
cleaned_response = cleaned_response[:-3]
|
|
36
|
+
cleaned_response = cleaned_response.strip()
|
|
37
|
+
|
|
38
|
+
cleaned_response = cleaned_response.replace("&", "&")
|
|
39
|
+
tree = ET.fromstring(cleaned_response)
|
|
40
|
+
result = {c.tag: c.text for c in tree}
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise ValueError(f"Invalid XML input: {e}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def execute(decoder: Decoder, **kwargs: dict[str, Any]) -> list[Variable]:
|
|
48
|
+
"""Execute a decoder step with the provided arguments.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
decoder: The decoder step to execute.
|
|
52
|
+
**kwargs: Additional keyword arguments.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
if len(decoder.inputs) != 1:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Decoder step {decoder.id} must have exactly one input, found {len(decoder.inputs)}."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# get the string value to decode
|
|
61
|
+
input = decoder.inputs[0].value
|
|
62
|
+
if not isinstance(input, str):
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Input to decoder step {decoder.id} must be a string, found {type(input).__name__}."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if decoder.format == DecoderFormat.json:
|
|
68
|
+
result_dict = parse_json(input)
|
|
69
|
+
elif decoder.format == DecoderFormat.xml:
|
|
70
|
+
result_dict = parse_xml(input)
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Unsupported decoder format: {decoder.format}. Supported formats are: {DecoderFormat.json}, {DecoderFormat.xml}."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Set the output variables with the parsed results
|
|
77
|
+
for output in decoder.outputs:
|
|
78
|
+
if output.id in result_dict:
|
|
79
|
+
output.value = result_dict[output.id]
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Output variable {output.id} not found in decoded result: {result_dict}"
|
|
83
|
+
)
|
|
84
|
+
return decoder.outputs
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
|
|
4
|
+
from llama_index.core.base.llms.types import ChatResponse, CompletionResponse
|
|
5
|
+
|
|
6
|
+
from qtype.dsl.domain_types import ChatMessage, Embedding
|
|
7
|
+
from qtype.interpreter.conversions import (
|
|
8
|
+
from_chat_message,
|
|
9
|
+
to_chat_message,
|
|
10
|
+
to_embedding_model,
|
|
11
|
+
to_llm,
|
|
12
|
+
to_memory,
|
|
13
|
+
)
|
|
14
|
+
from qtype.interpreter.exceptions import InterpreterError
|
|
15
|
+
from qtype.semantic.model import EmbeddingModel, LLMInference, Variable
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def execute(
|
|
21
|
+
li: LLMInference,
|
|
22
|
+
stream_fn: Callable | None = None,
|
|
23
|
+
**kwargs: dict[Any, Any],
|
|
24
|
+
) -> list[Variable]:
|
|
25
|
+
"""Execute a LLM inference step.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
li: The LLM inference step to execute.
|
|
29
|
+
**kwargs: Additional keyword arguments.
|
|
30
|
+
"""
|
|
31
|
+
logger.debug(f"Executing LLM inference step: {li.id}")
|
|
32
|
+
|
|
33
|
+
# Ensure we only have one output variable set.
|
|
34
|
+
if len(li.outputs) != 1:
|
|
35
|
+
raise InterpreterError(
|
|
36
|
+
"LLMInference step must have exactly one output variable."
|
|
37
|
+
)
|
|
38
|
+
output_variable = li.outputs[0]
|
|
39
|
+
|
|
40
|
+
# Determine if this is a chat session, completion, or embedding inference
|
|
41
|
+
if output_variable.type == Embedding:
|
|
42
|
+
if not isinstance(li.model, EmbeddingModel):
|
|
43
|
+
raise InterpreterError(
|
|
44
|
+
f"LLMInference step with Embedding output must use an embedding model, got {type(li.model)}"
|
|
45
|
+
)
|
|
46
|
+
if len(li.inputs) != 1:
|
|
47
|
+
raise InterpreterError(
|
|
48
|
+
"LLMInference step for completion must have exactly one input variable."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
input = li.inputs[0].value
|
|
52
|
+
model = to_embedding_model(li.model)
|
|
53
|
+
result = model.get_text_embedding(text=input)
|
|
54
|
+
output_variable.value = Embedding(
|
|
55
|
+
vector=result,
|
|
56
|
+
source_text=input if isinstance(input, str) else None,
|
|
57
|
+
metadata=None,
|
|
58
|
+
)
|
|
59
|
+
elif output_variable.type == ChatMessage:
|
|
60
|
+
model = to_llm(li.model, li.system_message)
|
|
61
|
+
|
|
62
|
+
if not all(
|
|
63
|
+
isinstance(input.value, ChatMessage) for input in li.inputs
|
|
64
|
+
):
|
|
65
|
+
raise InterpreterError(
|
|
66
|
+
f"LLMInference step with ChatMessage output must have ChatMessage inputs. Got {li.inputs}"
|
|
67
|
+
)
|
|
68
|
+
inputs = [
|
|
69
|
+
to_chat_message(input.value) # type: ignore
|
|
70
|
+
for input in li.inputs
|
|
71
|
+
] # type: ignore
|
|
72
|
+
|
|
73
|
+
# prepend the inputs with memory chat history if available
|
|
74
|
+
if li.memory:
|
|
75
|
+
# Note that the memory is cached in the resource cache, so this should persist for a while...
|
|
76
|
+
memory = to_memory(kwargs.get("session_id"), li.memory)
|
|
77
|
+
inputs = memory.get(inputs)
|
|
78
|
+
else:
|
|
79
|
+
memory = None
|
|
80
|
+
|
|
81
|
+
if stream_fn:
|
|
82
|
+
generator = model.stream_chat(
|
|
83
|
+
messages=inputs,
|
|
84
|
+
**(
|
|
85
|
+
li.model.inference_params
|
|
86
|
+
if li.model.inference_params
|
|
87
|
+
else {}
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
for chatResult in generator:
|
|
91
|
+
stream_fn(li, from_chat_message(chatResult.message))
|
|
92
|
+
else:
|
|
93
|
+
chatResult: ChatResponse = model.chat(
|
|
94
|
+
messages=inputs,
|
|
95
|
+
**(
|
|
96
|
+
li.model.inference_params
|
|
97
|
+
if li.model.inference_params
|
|
98
|
+
else {}
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
output_variable.value = from_chat_message(chatResult.message)
|
|
102
|
+
if memory:
|
|
103
|
+
memory.put([chatResult.message])
|
|
104
|
+
else:
|
|
105
|
+
model = to_llm(li.model, li.system_message)
|
|
106
|
+
|
|
107
|
+
if len(li.inputs) != 1:
|
|
108
|
+
raise InterpreterError(
|
|
109
|
+
"LLMInference step for completion must have exactly one input variable."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
input = li.inputs[0].value
|
|
113
|
+
if not isinstance(input, str):
|
|
114
|
+
logger.warning(
|
|
115
|
+
f"Input to LLMInference step {li.id} is not a string, converting: {input}"
|
|
116
|
+
)
|
|
117
|
+
input = str(input)
|
|
118
|
+
|
|
119
|
+
if stream_fn:
|
|
120
|
+
generator = model.stream_complete(prompt=input)
|
|
121
|
+
for completeResult in generator:
|
|
122
|
+
stream_fn(li, completeResult.delta)
|
|
123
|
+
else:
|
|
124
|
+
completeResult: CompletionResponse = model.complete(prompt=input)
|
|
125
|
+
output_variable.value = completeResult.text
|
|
126
|
+
|
|
127
|
+
return li.outputs # type: ignore[return-value]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import string
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from qtype.interpreter.exceptions import InterpreterError
|
|
6
|
+
from qtype.semantic.model import PromptTemplate, Variable
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_format_arguments(format_string: str) -> set[str]:
|
|
12
|
+
formatter = string.Formatter()
|
|
13
|
+
arguments = []
|
|
14
|
+
for literal_text, field_name, format_spec, conversion in formatter.parse(
|
|
15
|
+
format_string
|
|
16
|
+
):
|
|
17
|
+
if field_name:
|
|
18
|
+
arguments.append(field_name)
|
|
19
|
+
return set(arguments)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def execute(step: PromptTemplate, **kwargs: dict[str, Any]) -> list[Variable]:
|
|
23
|
+
"""Execute a prompt template step.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
step: The prompt template step to execute.
|
|
27
|
+
**kwargs: Additional keyword arguments.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
logger.debug(
|
|
31
|
+
f"Executing prompt template step: {step.id} with kwargs: {kwargs}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
format_args = get_format_arguments(step.template)
|
|
35
|
+
input_map = {
|
|
36
|
+
var.id: var.value
|
|
37
|
+
for var in step.inputs
|
|
38
|
+
if var.is_set() and var.id in format_args
|
|
39
|
+
}
|
|
40
|
+
missing = format_args - input_map.keys()
|
|
41
|
+
if missing:
|
|
42
|
+
raise InterpreterError(
|
|
43
|
+
f"The following fields are in the prompt template but not in the inputs: {missing}"
|
|
44
|
+
)
|
|
45
|
+
# Drop inputs that are not in format_args
|
|
46
|
+
result = step.template.format(**input_map)
|
|
47
|
+
|
|
48
|
+
if len(step.outputs) != 1:
|
|
49
|
+
raise InterpreterError(
|
|
50
|
+
f"PromptTemplate step {step.id} must have exactly one output variable."
|
|
51
|
+
)
|
|
52
|
+
step.outputs[0].value = result
|
|
53
|
+
|
|
54
|
+
return step.outputs # type: ignore[return-value]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from qtype.semantic.model import Search, Variable
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def execute(search: Search, **kwargs: dict[str, Any]) -> list[Variable]:
|
|
10
|
+
"""Execute a search step.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
search: The search step to execute.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
A list of variables that are set based on the search results.
|
|
17
|
+
"""
|
|
18
|
+
logger.info("Executing Search on: %s", search.index.id)
|
|
19
|
+
# TODO: implement search execution logic
|
|
20
|
+
raise NotImplementedError(
|
|
21
|
+
"Search execution is not yet implemented. This will be handled in a future update."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
return [] # Return an empty list for now
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from qtype.interpreter.exceptions import InterpreterError
|
|
5
|
+
from qtype.semantic.model import PythonFunctionTool, Tool, Variable
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def execute(tool: Tool, **kwargs: dict) -> list[Variable]:
|
|
11
|
+
"""Execute a tool step.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
tool: The tool step to execute.
|
|
15
|
+
**kwargs: Additional keyword arguments.
|
|
16
|
+
"""
|
|
17
|
+
logger.debug(f"Executing tool step: {tool.id} with kwargs: {kwargs}")
|
|
18
|
+
|
|
19
|
+
if isinstance(tool, PythonFunctionTool):
|
|
20
|
+
# import the function dynamically
|
|
21
|
+
module = importlib.import_module(tool.module_path)
|
|
22
|
+
function = getattr(module, tool.function_name, None)
|
|
23
|
+
if function is None:
|
|
24
|
+
raise InterpreterError(
|
|
25
|
+
f"Function {tool.function_name} not found in {tool.module_path}"
|
|
26
|
+
)
|
|
27
|
+
# Call the function with the provided arguments
|
|
28
|
+
if any(not inputs.is_set() for inputs in tool.inputs):
|
|
29
|
+
raise InterpreterError(
|
|
30
|
+
f"Tool {tool.id} requires all inputs to be set. Missing inputs: {[var.id for var in tool.inputs if not var.is_set()]}"
|
|
31
|
+
)
|
|
32
|
+
inputs = {var.id: var.value for var in tool.inputs if var.is_set()}
|
|
33
|
+
results = function(**inputs)
|
|
34
|
+
else:
|
|
35
|
+
# TODO: support api tools
|
|
36
|
+
raise InterpreterError(f"Unsupported tool type: {type(tool).__name__}")
|
|
37
|
+
|
|
38
|
+
if isinstance(results, dict) and len(tool.outputs) > 1:
|
|
39
|
+
for var in tool.outputs:
|
|
40
|
+
if var.id in results:
|
|
41
|
+
var.value = results[var.id]
|
|
42
|
+
else:
|
|
43
|
+
raise InterpreterError(
|
|
44
|
+
f"Output variable {var.id} not found in function results."
|
|
45
|
+
)
|
|
46
|
+
elif len(tool.outputs) == 1:
|
|
47
|
+
tool.outputs[0].value = results
|
|
48
|
+
else:
|
|
49
|
+
raise InterpreterError(
|
|
50
|
+
f"The returned value {results} could not be assigned to outputs {[var.id for var in tool.outputs]}."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
return tool.outputs # type: ignore[return-value]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from openinference.instrumentation.llama_index import LlamaIndexInstrumentor
|
|
2
|
+
from phoenix.otel import register as register_phoenix
|
|
3
|
+
|
|
4
|
+
from qtype.semantic.model import TelemetrySink
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def register(telemetry: TelemetrySink, project_id: str | None = None) -> None:
|
|
8
|
+
"""Register a telemetry instance."""
|
|
9
|
+
|
|
10
|
+
# Only llama_index and phoenix are supported for now
|
|
11
|
+
# TODO: Add support for langfues and llamatrace
|
|
12
|
+
tracer_provider = register_phoenix(
|
|
13
|
+
endpoint=telemetry.endpoint,
|
|
14
|
+
project_name=project_id if project_id else telemetry.id,
|
|
15
|
+
)
|
|
16
|
+
LlamaIndexInstrumentor().instrument(tracer_provider=tracer_provider)
|