Spaces:
Runtime error
Runtime error
from typing import Any, Optional, Dict, List, Union, Callable, cast | |
from typing_extensions import Literal | |
from pydantic import BaseModel, ConfigDict, model_validator | |
from phi.assistant.openai.assistant import OpenAIAssistant | |
from phi.assistant.openai.exceptions import ThreadIdNotSet, AssistantIdNotSet, RunIdNotSet | |
from phi.tools import Tool, Toolkit | |
from phi.tools.function import Function | |
from phi.utils.functions import get_function_call | |
from phi.utils.log import logger | |
try: | |
from openai import OpenAI | |
from openai.types.beta.threads.run import ( | |
Run as OpenAIRun, | |
RequiredAction, | |
LastError, | |
) | |
from openai.types.beta.threads.required_action_function_tool_call import RequiredActionFunctionToolCall | |
from openai.types.beta.threads.run_submit_tool_outputs_params import ToolOutput | |
except ImportError: | |
logger.error("`openai` not installed") | |
raise | |
class Run(BaseModel): | |
# -*- Run settings | |
# Run id which can be referenced in API endpoints. | |
id: Optional[str] = None | |
# The object type, populated by the API. Always assistant.run. | |
object: Optional[str] = None | |
# The ID of the thread that was executed on as a part of this run. | |
thread_id: Optional[str] = None | |
# OpenAIAssistant used for this run | |
assistant: Optional[OpenAIAssistant] = None | |
# The ID of the assistant used for execution of this run. | |
assistant_id: Optional[str] = None | |
# The status of the run, which can be either | |
# queued, in_progress, requires_action, cancelling, cancelled, failed, completed, or expired. | |
status: Optional[ | |
Literal["queued", "in_progress", "requires_action", "cancelling", "cancelled", "failed", "completed", "expired"] | |
] = None | |
# Details on the action required to continue the run. Will be null if no action is required. | |
required_action: Optional[RequiredAction] = None | |
# The Unix timestamp (in seconds) for when the run was created. | |
created_at: Optional[int] = None | |
# The Unix timestamp (in seconds) for when the run was started. | |
started_at: Optional[int] = None | |
# The Unix timestamp (in seconds) for when the run will expire. | |
expires_at: Optional[int] = None | |
# The Unix timestamp (in seconds) for when the run was cancelled. | |
cancelled_at: Optional[int] = None | |
# The Unix timestamp (in seconds) for when the run failed. | |
failed_at: Optional[int] = None | |
# The Unix timestamp (in seconds) for when the run was completed. | |
completed_at: Optional[int] = None | |
# The list of File IDs the assistant used for this run. | |
file_ids: Optional[List[str]] = None | |
# The ID of the Model to be used to execute this run. If a value is provided here, | |
# it will override the model associated with the assistant. | |
# If not, the model associated with the assistant will be used. | |
model: Optional[str] = None | |
# Override the default system message of the assistant. | |
# This is useful for modifying the behavior on a per-run basis. | |
instructions: Optional[str] = None | |
# Override the tools the assistant can use for this run. | |
# This is useful for modifying the behavior on a per-run basis. | |
tools: Optional[List[Union[Tool, Toolkit, Callable, Dict, Function]]] = None | |
# Functions extracted from the tools which can be executed locally by the assistant. | |
functions: Optional[Dict[str, Function]] = None | |
# The last error associated with this run. Will be null if there are no errors. | |
last_error: Optional[LastError] = None | |
# Set of 16 key-value pairs that can be attached to an object. | |
# This can be useful for storing additional information about the object in a structured format. | |
# Keys can be a maximum of 64 characters long and values can be a maximum of 512 characters long. | |
metadata: Optional[Dict[str, Any]] = None | |
# If True, show debug logs | |
debug_mode: bool = False | |
# Enable monitoring on phidata.com | |
monitoring: bool = False | |
openai: Optional[OpenAI] = None | |
openai_run: Optional[OpenAIRun] = None | |
model_config = ConfigDict(arbitrary_types_allowed=True) | |
def client(self) -> OpenAI: | |
return self.openai or OpenAI() | |
def extract_functions_from_tools(self) -> "Run": | |
if self.tools is not None: | |
for tool in self.tools: | |
if self.functions is None: | |
self.functions = {} | |
if isinstance(tool, Toolkit): | |
self.functions.update(tool.functions) | |
logger.debug(f"Functions from {tool.name} added to OpenAIAssistant.") | |
elif isinstance(tool, Function): | |
self.functions[tool.name] = tool | |
logger.debug(f"Function {tool.name} added to OpenAIAssistant.") | |
elif callable(tool): | |
f = Function.from_callable(tool) | |
self.functions[f.name] = f | |
logger.debug(f"Function {f.name} added to OpenAIAssistant") | |
return self | |
def load_from_openai(self, openai_run: OpenAIRun): | |
self.id = openai_run.id | |
self.object = openai_run.object | |
self.status = openai_run.status | |
self.required_action = openai_run.required_action | |
self.last_error = openai_run.last_error | |
self.created_at = openai_run.created_at | |
self.started_at = openai_run.started_at | |
self.expires_at = openai_run.expires_at | |
self.cancelled_at = openai_run.cancelled_at | |
self.failed_at = openai_run.failed_at | |
self.completed_at = openai_run.completed_at | |
self.file_ids = openai_run.file_ids | |
self.openai_run = openai_run | |
def get_tools_for_api(self) -> Optional[List[Dict[str, Any]]]: | |
if self.tools is None: | |
return None | |
tools_for_api = [] | |
for tool in self.tools: | |
if isinstance(tool, Tool): | |
tools_for_api.append(tool.to_dict()) | |
elif isinstance(tool, dict): | |
tools_for_api.append(tool) | |
elif callable(tool): | |
func = Function.from_callable(tool) | |
tools_for_api.append({"type": "function", "function": func.to_dict()}) | |
elif isinstance(tool, Toolkit): | |
for _f in tool.functions.values(): | |
tools_for_api.append({"type": "function", "function": _f.to_dict()}) | |
elif isinstance(tool, Function): | |
tools_for_api.append({"type": "function", "function": tool.to_dict()}) | |
return tools_for_api | |
def create( | |
self, | |
thread_id: Optional[str] = None, | |
assistant: Optional[OpenAIAssistant] = None, | |
assistant_id: Optional[str] = None, | |
) -> "Run": | |
_thread_id = thread_id or self.thread_id | |
if _thread_id is None: | |
raise ThreadIdNotSet("Thread.id not set") | |
_assistant_id = assistant.get_id() if assistant is not None else assistant_id | |
if _assistant_id is None: | |
_assistant_id = self.assistant.get_id() if self.assistant is not None else self.assistant_id | |
if _assistant_id is None: | |
raise AssistantIdNotSet("OpenAIAssistant.id not set") | |
request_body: Dict[str, Any] = {} | |
if self.model is not None: | |
request_body["model"] = self.model | |
if self.instructions is not None: | |
request_body["instructions"] = self.instructions | |
if self.tools is not None: | |
request_body["tools"] = self.get_tools_for_api() | |
if self.metadata is not None: | |
request_body["metadata"] = self.metadata | |
self.openai_run = self.client.beta.threads.runs.create( | |
thread_id=_thread_id, assistant_id=_assistant_id, **request_body | |
) | |
self.load_from_openai(self.openai_run) # type: ignore | |
logger.debug(f"Run created: {self.id}") | |
return self | |
def get_id(self) -> Optional[str]: | |
return self.id or self.openai_run.id if self.openai_run else None | |
def get_from_openai(self, thread_id: Optional[str] = None) -> OpenAIRun: | |
_thread_id = thread_id or self.thread_id | |
if _thread_id is None: | |
raise ThreadIdNotSet("Thread.id not set") | |
_run_id = self.get_id() | |
if _run_id is None: | |
raise RunIdNotSet("Run.id not set") | |
self.openai_run = self.client.beta.threads.runs.retrieve( | |
thread_id=_thread_id, | |
run_id=_run_id, | |
) | |
self.load_from_openai(self.openai_run) | |
return self.openai_run | |
def get(self, use_cache: bool = True, thread_id: Optional[str] = None) -> "Run": | |
if self.openai_run is not None and use_cache: | |
return self | |
self.get_from_openai(thread_id=thread_id) | |
return self | |
def get_or_create( | |
self, | |
use_cache: bool = True, | |
thread_id: Optional[str] = None, | |
assistant: Optional[OpenAIAssistant] = None, | |
assistant_id: Optional[str] = None, | |
) -> "Run": | |
try: | |
return self.get(use_cache=use_cache) | |
except RunIdNotSet: | |
return self.create(thread_id=thread_id, assistant=assistant, assistant_id=assistant_id) | |
def update(self, thread_id: Optional[str] = None) -> "Run": | |
try: | |
run_to_update = self.get_from_openai(thread_id=thread_id) | |
if run_to_update is not None: | |
request_body: Dict[str, Any] = {} | |
if self.metadata is not None: | |
request_body["metadata"] = self.metadata | |
self.openai_run = self.client.beta.threads.runs.update( | |
thread_id=run_to_update.thread_id, | |
run_id=run_to_update.id, | |
**request_body, | |
) | |
self.load_from_openai(self.openai_run) | |
logger.debug(f"Run updated: {self.id}") | |
return self | |
raise ValueError("Run not available") | |
except (ThreadIdNotSet, RunIdNotSet): | |
logger.warning("Message not available") | |
raise | |
def wait( | |
self, | |
interval: int = 1, | |
timeout: Optional[int] = None, | |
thread_id: Optional[str] = None, | |
status: Optional[List[str]] = None, | |
callback: Optional[Callable[[OpenAIRun], None]] = None, | |
) -> bool: | |
import time | |
status_to_wait = status or ["requires_action", "cancelling", "cancelled", "failed", "completed", "expired"] | |
start_time = time.time() | |
while True: | |
logger.debug(f"Waiting for run {self.id} to complete") | |
run = self.get_from_openai(thread_id=thread_id) | |
logger.debug(f"Run {run.id} {run.status}") | |
if callback is not None: | |
callback(run) | |
if run.status in status_to_wait: | |
return True | |
if timeout is not None and time.time() - start_time > timeout: | |
logger.error(f"Run {run.id} did not complete within {timeout} seconds") | |
return False | |
# raise TimeoutError(f"Run {run.id} did not complete within {timeout} seconds") | |
time.sleep(interval) | |
def run( | |
self, | |
thread_id: Optional[str] = None, | |
assistant: Optional[OpenAIAssistant] = None, | |
assistant_id: Optional[str] = None, | |
wait: bool = True, | |
callback: Optional[Callable[[OpenAIRun], None]] = None, | |
) -> "Run": | |
# Update Run with new values | |
self.thread_id = thread_id or self.thread_id | |
self.assistant = assistant or self.assistant | |
self.assistant_id = assistant_id or self.assistant_id | |
# Create Run | |
self.create() | |
run_completed = not wait | |
while not run_completed: | |
self.wait(callback=callback) | |
# -*- Check if run requires action | |
if self.status == "requires_action": | |
if self.assistant is None: | |
logger.warning("OpenAIAssistant not available to complete required_action") | |
return self | |
if self.required_action is not None: | |
if self.required_action.type == "submit_tool_outputs": | |
tool_calls: List[RequiredActionFunctionToolCall] = ( | |
self.required_action.submit_tool_outputs.tool_calls | |
) | |
tool_outputs = [] | |
for tool_call in tool_calls: | |
if tool_call.type == "function": | |
run_functions = self.assistant.functions | |
if self.functions is not None: | |
if run_functions is not None: | |
run_functions.update(self.functions) | |
else: | |
run_functions = self.functions | |
function_call = get_function_call( | |
name=tool_call.function.name, | |
arguments=tool_call.function.arguments, | |
functions=run_functions, | |
) | |
if function_call is None: | |
logger.error(f"Function {tool_call.function.name} not found") | |
continue | |
# -*- Run function call | |
success = function_call.execute() | |
if not success: | |
logger.error(f"Function {tool_call.function.name} failed") | |
continue | |
output = str(function_call.result) if function_call.result is not None else "" | |
tool_outputs.append(ToolOutput(tool_call_id=tool_call.id, output=output)) | |
# -*- Submit tool outputs | |
_oai_run = cast(OpenAIRun, self.openai_run) | |
self.openai_run = self.client.beta.threads.runs.submit_tool_outputs( | |
thread_id=_oai_run.thread_id, | |
run_id=_oai_run.id, | |
tool_outputs=tool_outputs, | |
) | |
self.load_from_openai(self.openai_run) | |
else: | |
run_completed = True | |
return self | |
def to_dict(self) -> Dict[str, Any]: | |
return self.model_dump( | |
exclude_none=True, | |
include={ | |
"id", | |
"object", | |
"thread_id", | |
"assistant_id", | |
"status", | |
"required_action", | |
"last_error", | |
"model", | |
"instructions", | |
"tools", | |
"metadata", | |
}, | |
) | |
def pprint(self): | |
"""Pretty print using rich""" | |
from rich.pretty import pprint | |
pprint(self.to_dict()) | |
def __str__(self) -> str: | |
import json | |
return json.dumps(self.to_dict(), indent=4) | |