Skip to content

Commit c51cc8c

Browse files
committed
feat: websocket calls multiple agents and returns messages
1 parent 1b76225 commit c51cc8c

6 files changed

Lines changed: 534 additions & 141 deletions

File tree

agentmesh/api/websocket_api.py

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import uuid
22
import json
3+
import threading
34
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
45
from fastapi.responses import JSONResponse
56

6-
from ..service.websocket_service import websocket_manager, task_processor
7+
from ..service.websocket_service import websocket_manager, task_processor, thread_manager
78
from ..common.models import UserInputMessage
89

910
# Create router
@@ -21,19 +22,66 @@ async def websocket_endpoint(websocket: WebSocket):
2122

2223
try:
2324
# Accept the WebSocket connection
24-
await websocket_manager.connect(websocket, connection_id)
25+
await websocket.accept()
26+
27+
# Create a simple connection wrapper for FastAPI WebSocket
28+
class FastAPIWebSocketWrapper:
29+
def __init__(self, websocket: WebSocket):
30+
self.websocket = websocket
31+
self.connection_id = connection_id
32+
33+
def send(self, message: str):
34+
"""Send message using FastAPI WebSocket"""
35+
import asyncio
36+
try:
37+
# Create a new event loop for this thread
38+
loop = asyncio.new_event_loop()
39+
asyncio.set_event_loop(loop)
40+
loop.run_until_complete(self.websocket.send_text(message))
41+
except Exception as e:
42+
print(f"Error sending message: {e}")
43+
finally:
44+
try:
45+
loop.close()
46+
except:
47+
pass
48+
49+
# Create wrapper and register with manager
50+
ws_wrapper = FastAPIWebSocketWrapper(websocket)
51+
websocket_manager.connect(ws_wrapper, connection_id)
2552
print(f"WebSocket client connected: {connection_id}")
2653

2754
# Handle messages from client
2855
while True:
2956
try:
57+
# Check if shutdown is requested
58+
if thread_manager.shutdown_event.is_set():
59+
print(f"Shutdown requested, closing connection {connection_id}")
60+
break
61+
3062
# Receive message from client
3163
data = await websocket.receive_text()
64+
65+
# Process message
3266
message_data = json.loads(data)
3367

3468
# Handle different message types
3569
if message_data.get("event") == "user_input":
36-
await handle_user_input(connection_id, message_data)
70+
# Start task execution in a separate thread with proper tracking
71+
task_thread = threading.Thread(
72+
target=handle_user_input,
73+
args=(connection_id, message_data),
74+
name=f"task-{connection_id}",
75+
daemon=False # Changed to False for better control
76+
)
77+
78+
# Add thread to manager for tracking
79+
thread_manager.add_thread(task_thread)
80+
task_thread.start()
81+
82+
# Clean up completed threads
83+
thread_manager.remove_thread(task_thread)
84+
3785
else:
3886
print(f"Unknown event type: {message_data.get('event')}")
3987

@@ -55,27 +103,32 @@ async def websocket_endpoint(websocket: WebSocket):
55103
print(f"WebSocket connection cleaned up: {connection_id}")
56104

57105

58-
async def handle_user_input(connection_id: str, message_data: dict):
106+
def handle_user_input(connection_id: str, message_data: dict):
59107
"""Handle user input message and start task execution"""
60108
try:
109+
# Check if shutdown is requested
110+
if thread_manager.shutdown_event.is_set():
111+
print(f"Shutdown requested, skipping task for connection {connection_id}")
112+
return
113+
61114
# Extract user input data
62115
user_input = message_data.get("data", {})
63116
text = user_input.get("text", "")
117+
team_name = user_input.get("team", "general_team") # Default to general_team
64118

65119
if not text:
66120
print("Empty user input received")
67121
return
68122

69-
print(f"Processing user input: {text[:50]}...")
123+
print(f"Processing user input: {text[:50]}... with team: {team_name}")
70124

71125
# Process user input and create task
72-
task_id = await task_processor.process_user_input(connection_id, user_input)
126+
task_id = task_processor.process_user_input(connection_id, user_input)
73127

74-
# Start task execution in background
75-
import asyncio
76-
asyncio.create_task(task_processor.execute_task(task_id, text))
128+
# Execute task synchronously (this will stream results via WebSocket)
129+
task_processor.execute_task(task_id, text, team_name)
77130

78-
print(f"Task {task_id} started for connection {connection_id}")
131+
print(f"Task {task_id} completed for connection {connection_id} with team {team_name}")
79132

80133
except Exception as e:
81134
print(f"Error handling user input: {e}")
@@ -87,4 +140,4 @@ async def handle_user_input(connection_id: str, message_data: dict):
87140
"msg": f"Failed to process task: {str(e)}"
88141
}
89142
}
90-
await websocket_manager.send_message(connection_id, error_message)
143+
websocket_manager.send_message(connection_id, error_message)

agentmesh/protocol/agent.py

Lines changed: 40 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(self, name: str, system_prompt: str, description: str, model: LLMMo
3838
self.max_steps = max_steps # max ReAct steps, None means no limit
3939
self.conversation_history = []
4040
self.action_history = []
41+
self.captured_actions = [] # Initialize captured actions list
4142
self.ext_data = ""
4243
self.output_mode = output_mode
4344
if tools:
@@ -140,10 +141,6 @@ def step(self):
140141
final_answer = None
141142
current_step = 0
142143

143-
# Initialize captured actions list (if it doesn't exist)
144-
if not hasattr(self, 'captured_actions'):
145-
self.captured_actions = []
146-
147144
# Initialize final answer (if it doesn't exist)
148145
if not hasattr(self, 'final_answer'):
149146
self.final_answer = ""
@@ -277,7 +274,25 @@ def step(self):
277274
tool: BaseTool = self._find_tool(parsed["action"])
278275
observation = ""
279276
if tool:
277+
# Record start time for execution timing
278+
start_time = time.time()
279+
280280
tool_result = tool.execute_tool(parsed.get("action_input", {}))
281+
282+
# Calculate execution time
283+
execution_time = time.time() - start_time
284+
285+
# Capture tool use for tracking
286+
self.capture_tool_use(
287+
tool_name=parsed["action"],
288+
input_params=parsed.get("action_input", {}),
289+
thought=parsed['thought'],
290+
output=tool_result.result,
291+
status=tool_result.status,
292+
error_message=tool_result.result if tool_result.status == "error" else None,
293+
execution_time=execution_time,
294+
)
295+
281296
# Update conversation history
282297
parsed["Observation"] = {
283298
"status": tool_result.status,
@@ -326,9 +341,25 @@ def _execute_post_process_tools(self):
326341
# Set tool context
327342
tool.context = self
328343

344+
# Record start time for execution timing
345+
start_time = time.time()
346+
329347
# Execute tool (with empty parameters, tool will extract needed info from context)
330348
result = tool.execute({})
331349

350+
# Calculate execution time
351+
execution_time = time.time() - start_time
352+
353+
# Capture tool use for tracking
354+
self.capture_tool_use(
355+
tool_name=tool.name,
356+
input_params={}, # Post-process tools typically don't take parameters
357+
output=result.result,
358+
status=result.status,
359+
error_message=str(result.result) if result.status == "error" else None,
360+
execution_time=execution_time
361+
)
362+
332363
# Log result
333364
if result.status == "success":
334365
# Print tool execution result in the desired format
@@ -424,10 +455,12 @@ def _fetch_agents_outputs(self) -> str:
424455
f"member name: {agent_output.agent_name}\noutput content: {agent_output.output}\n\n")
425456
return "\n".join(agent_outputs_list)
426457

427-
def capture_tool_use(self, tool_name, input_params, output, status, error_message=None, execution_time=0.0):
458+
def capture_tool_use(self, tool_name, input_params, output, status, thought=None, error_message=None,
459+
execution_time=0.0):
428460
"""
429461
Capture a tool use action.
430462
463+
:param thought: thought content
431464
:param tool_name: Name of the tool used
432465
:param input_params: Parameters passed to the tool
433466
:param output: Output from the tool
@@ -448,52 +481,10 @@ def capture_tool_use(self, tool_name, input_params, output, status, error_messag
448481
agent_id=self.id if hasattr(self, 'id') else str(id(self)),
449482
agent_name=self.name,
450483
action_type=AgentActionType.TOOL_USE,
451-
tool_result=tool_result
484+
tool_result=tool_result,
485+
thought=thought
452486
)
453487

454-
if not hasattr(self, 'captured_actions'):
455-
self.captured_actions = []
456-
457-
self.captured_actions.append(action)
458-
459-
return action
460-
461-
def capture_thinking(self, thought_content):
462-
"""
463-
Capture a thinking action.
464-
465-
:param thought_content: Content of the thought
466-
"""
467-
action = AgentAction(
468-
agent_id=self.id if hasattr(self, 'id') else str(id(self)),
469-
agent_name=self.name,
470-
action_type=AgentActionType.THINKING,
471-
content=thought_content
472-
)
473-
474-
if not hasattr(self, 'captured_actions'):
475-
self.captured_actions = []
476-
477-
self.captured_actions.append(action)
478-
479-
return action
480-
481-
def capture_final_answer(self, answer_content):
482-
"""
483-
Capture a final answer action.
484-
485-
:param answer_content: Content of the final answer
486-
"""
487-
action = AgentAction(
488-
agent_id=self.id if hasattr(self, 'id') else str(id(self)),
489-
agent_name=self.name,
490-
action_type=AgentActionType.FINAL_ANSWER,
491-
content=answer_content
492-
)
493-
494-
if not hasattr(self, 'captured_actions'):
495-
self.captured_actions = []
496-
497488
self.captured_actions.append(action)
498489

499490
return action

agentmesh/protocol/result.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class AgentAction:
5555
id: str = field(default_factory=lambda: str(uuid.uuid4()))
5656
content: str = ""
5757
tool_result: Optional[ToolResult] = None
58+
thought: Optional[str] = None
5859
timestamp: float = field(default_factory=time.time)
5960

6061

@@ -113,13 +114,14 @@ def to_dict(self) -> Dict[str, Any]:
113114
"agent_name": action.agent_name,
114115
"action_type": action.action_type.value,
115116
"content": action.content,
117+
"thought": action.thought,
116118
"tool_result": {
117119
"tool_name": action.tool_result.tool_name,
118120
"input_params": action.tool_result.input_params,
119121
"output": action.tool_result.output,
120122
"status": action.tool_result.status,
121123
"error_message": action.tool_result.error_message,
122-
"execution_time": action.tool_result.execution_time
124+
"execution_time": action.tool_result.execution_time,
123125
} if action.tool_result else None,
124126
"timestamp": action.timestamp
125127
}

0 commit comments

Comments
 (0)