Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 26 additions & 116 deletions src/openserv_sdk/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,85 +361,10 @@ async def do_task(self, action: DoTaskAction) -> None:
# Execute the task and let the runtime handle the response
logger.info(f"Executing task {action.task.id}")

tools_json = [Agent._convert_tool_to_json_schema(t) for t in self._tools]

action_data = {
'type': action.type,
'me': {
'id': action.me.id,
'name': action.me.name,
'kind': action.me.kind,
'is_built_by_agent_builder': action.me.is_built_by_agent_builder,
'system_prompt': action.me.system_prompt,
'capabilities_description': action.me.capabilities_description
},
'task': {
'id': action.task.id,
'description': action.task.description,
'body': action.task.body,
'expected_output': action.task.expected_output,
'input': action.task.input,
'dependencies': [
{
'id': d.id,
'description': d.description,
'output': d.output,
'status': d.status,
'attachments': [
{
'id': a.id,
'path': a.path,
'full_url': a.full_url,
'summary': a.summary
} for a in d.attachments
]
} for d in action.task.dependencies
],
'human_assistance_requests': [
{
'id': r.id,
'agent_dump': r.agent_dump,
'human_response': r.human_response,
'question': r.question,
'status': r.status,
'type': r.type
} for r in action.task.human_assistance_requests
]
},
'workspace': {
'id': action.workspace.id,
'goal': action.workspace.goal,
'bucket_folder': action.workspace.bucket_folder,
'agents': [
{
'id': a.id,
'name': a.name,
'kind': a.kind,
'capabilities_description': a.capabilities_description
} for a in action.workspace.agents
]
},
'integrations': [
{
'id': i.id,
'connection_id': i.connection_id,
'provider_config_key': i.provider_config_key,
'provider': i.provider,
'created': i.created,
'metadata': i.metadata,
'scopes': i.scopes,
'open_api': i.open_api
} for i in action.integrations
],
'memories': [
{
'id': m.id,
'memory': m.memory,
'created_at': m.created_at.isoformat()
} for m in action.memories
]
}
tools_json = [self._convert_tool_to_json_schema(t) for t in self._tools]

action_data = action.model_dump()

logger.info(f"Tools JSON: {json.dumps(tools_json, indent=2)}")
logger.info(f"Messages: {json.dumps(messages, indent=2)}")
logger.info(f"Action data: {json.dumps(action_data, indent=2)}")
Expand Down Expand Up @@ -487,15 +412,14 @@ async def respond_to_chat(self, action: RespondChatMessageAction) -> None:
for msg in action.messages:
messages.append({
'role': 'user' if msg.author == 'user' else 'assistant',
'content': msg.message,
'id': msg.id,
'created_at': msg.created_at.isoformat()
'content': msg.message
# Remove id and created_at - they're not expected by the API
})

try:
# Get the chat response
response = await self._runtime_client.handle_chat(
tools=[Agent._convert_tool_to_json_schema(t) for t in self._tools],
tools=[self._convert_tool_to_json_schema(t) for t in self._tools],
messages=messages,
action=action.model_dump()
)
Expand All @@ -511,21 +435,30 @@ async def respond_to_chat(self, action: RespondChatMessageAction) -> None:
logger.error("Chat response failed: %s", str(error), exc_info=True)
# Don't re-raise the error to match TypeScript behavior

@staticmethod
def _convert_tool_to_json_schema(tool: Capability[BaseModel]) -> Dict[str, Any]:
"""Convert a tool to JSON schema format."""
schema = tool.schema.model_json_schema()
# Remove title from properties if present
if "properties" in schema:
for prop in schema["properties"].values():
if isinstance(prop, dict):
prop.pop("title", None)

return {
'name': tool.name,
'description': tool.description,
'parameters': schema
'parameters': tool.schema.model_json_schema()
}

def convert_to_openai_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Convert tools to OpenAI format."""
openai_tools = []
for tool in tools:
openai_tool = {
"type": "function",
"function": {
"name": tool["name"],
"description": tool["description"],
"parameters": tool["parameters"]
}
}
openai_tools.append(openai_tool)
return openai_tools

async def get_files(self, workspace_id: Union[int, GetFilesParams]) -> Dict[str, Any]:
"""Get files in a workspace."""
if isinstance(workspace_id, GetFilesParams):
Expand Down Expand Up @@ -615,19 +548,18 @@ async def mark_task_as_errored(self, workspace_id: int, task_id: int, error: str
"""Mark a task as errored with the given error message."""
try:
response = await self._api_client.post(
f"/workspaces/{workspace_id}/task/{task_id}/error",
f"/workspaces/{workspace_id}/tasks/{task_id}/error",
{"error": error}
)
return response

except Exception as e:
logger.error(f"Failed to mark task as errored: {str(e)}")
return {"status": "error", "error": str(e)}

async def complete_task(self, workspace_id: int, task_id: int, output: str) -> Dict[str, Any]:
"""Complete a task."""
response = await self._api_client.post(
f"/workspaces/{workspace_id}/task/{task_id}/complete",
f"/workspaces/{workspace_id}/tasks/{task_id}/complete",
{"output": output}
)
return response["data"]
Expand Down Expand Up @@ -695,11 +627,10 @@ async def update_task_status(self, params: UpdateTaskStatusParams) -> Dict[str,
"""Update a task's status."""
try:
response = await self._api_client.post(
f"/workspaces/{params.workspace_id}/task/{params.task_id}/status",
f"/workspaces/{params.workspace_id}/tasks/{params.task_id}/status",
{"status": params.status.value if isinstance(params.status, TaskStatus) else params.status}
)
return response["data"]

except Exception as e:
logger.error(f"Failed to update task status: {str(e)}")
return {"status": "error", "error": str(e)}
Expand All @@ -714,24 +645,3 @@ async def call_integration(self, integration: IntegrationCallRequest) -> Dict[st
integration.details.model_dump()
)
return response["data"]

def convert_to_openai_tools(self, tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Convert tools to OpenAI format."""
openai_tools = []
for tool in tools:
# Create a copy of the parameters to avoid modifying the original
parameters = tool["parameters"].copy()
# Remove title field if present
if "title" in parameters:
del parameters["title"]

openai_tool = {
"type": "function",
"function": {
"name": tool["name"],
"description": tool["description"],
"parameters": parameters
}
}
openai_tools.append(openai_tool)
return openai_tools
53 changes: 44 additions & 9 deletions src/openserv_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,26 +197,38 @@ async def execute_task(
) -> Dict[str, Any]:
"""Execute a task."""
url = f"{self.config.runtime_url}/runtime/execute"

# Convert tools to OpenAI function format
formatted_tools = []
for tool in tools:
formatted_tool = {
"type": "function",
"function": {
"name": tool["name"],
"description": tool["description"],
"parameters": tool["parameters"]
}
}
formatted_tools.append(formatted_tool)

payload = {
'workspaceId': workspace_id,
'taskId': task_id,
'tools': tools,
'tools': formatted_tools,
'messages': messages,
'action': action
}

logger.info(f"Executing task with payload: {json.dumps(payload, indent=2)}")
try:
response = await self._request('POST', url, json_data=payload)
if isinstance(response, bytes):
return {"status": "success"}
logger.info(f"Task execution response: {json.dumps(response, indent=2) if response else 'None'}")
return response or {}
response = response.decode('utf-8')
return response if response else {}
except Exception as e:
logger.error(f"Failed to execute task: {str(e)}")
if isinstance(e, httpx.HTTPStatusError):
error_content = e.response.content.decode('utf-8') if e.response.content else "No content"
logger.error(f"Response content: {error_content}")
raise APIError(f"Failed to execute task: {error_content}")
logger.error(f"Response content: {e.response.content}")
raise APIError(f"Failed to execute task: {str(e)}")

async def handle_chat(
Expand All @@ -227,8 +239,31 @@ async def handle_chat(
) -> Dict[str, Any]:
"""Handle a chat message."""
url = f"{self.config.runtime_url}/runtime/chat"
return await self._request('POST', url, json_data={

# Add required fields to action if not present
if isinstance(action, dict):
if 'workspaceId' not in action and 'workspace' in action:
action['workspaceId'] = action['workspace'].get('id')
if 'taskId' not in action and 'task' in action:
action['taskId'] = action['task'].get('id')

payload = {
'workspaceId': action.get('workspaceId'),
'taskId': action.get('taskId'),
'tools': tools,
'messages': messages,
'action': action
})
}

# Log the exact payload being sent
logger.info(f"Sending chat request with payload: {json.dumps(payload, indent=2)}")

try:
response = await self._request('POST', url, json_data=payload)
logger.info(f"Chat response: {json.dumps(response, indent=2) if response else 'None'}")
return response
except Exception as e:
logger.error(f"Chat request failed with error: {str(e)}")
if isinstance(e, httpx.HTTPStatusError):
logger.error(f"Response content: {e.response.content}")
raise