Skip to content
Open
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
41 changes: 16 additions & 25 deletions src/thenvoi/adapters/crewai.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,15 @@ async def on_started(self, agent_name: str, agent_description: str) -> None:
goal = self.goal or agent_description or "Help users accomplish their tasks"

if self.backstory:
# User provided full backstory -- append capability-gated platform
# instructions so the LLM knows about memory/contact tools if enabled.
# User provided full backstory -- prepend capability-gated platform
# instructions so Thenvoi rules land before custom backstory text.
platform_prompt = render_system_prompt(
agent_name=agent_name,
agent_description=agent_description,
custom_section=self.custom_section or "",
features=self.features,
)
backstory = f"{self.backstory}\n\n{platform_prompt}"
backstory = f"{platform_prompt}\n\n{self.backstory}"
else:
backstory = render_system_prompt(
agent_name=agent_name,
Expand Down Expand Up @@ -305,9 +305,7 @@ async def _process_message(
"""Internal message processing logic."""
if is_session_bootstrap:
if history:
self._message_history[room_id] = [
{"role": h["role"], "content": h["content"]} for h in history
]
self._message_history[room_id] = [dict(h) for h in history]
logger.info(
"Room %s: Loaded %s historical messages",
room_id,
Expand All @@ -317,50 +315,43 @@ async def _process_message(
self._message_history[room_id] = []
logger.info("Room %s: No historical messages found", room_id)
elif room_id not in self._message_history:
self._message_history[room_id] = []

messages = []

if is_session_bootstrap and self._message_history.get(room_id):
history_text = "\n".join(
f"{m['role']}: {m['content']}" for m in self._message_history[room_id]
)
messages.append(
{
"role": "user",
"content": f"[Previous conversation:]\n{history_text}",
}
)
self._message_history[room_id] = [dict(h) for h in history]

if participants_msg:
messages.append(
self._message_history[room_id].append(
{
"role": "user",
"content": f"[System]: {participants_msg}",
"sender": "System",
"sender_type": "System",
}
)
logger.info("Room %s: Participants updated", room_id)

if contacts_msg:
messages.append(
self._message_history[room_id].append(
{
"role": "user",
"content": f"[System]: {contacts_msg}",
"sender": "System",
"sender_type": "System",
}
)
logger.info("Room %s: Contacts broadcast received", room_id)

user_message = msg.format_for_llm()
messages.append({"role": "user", "content": user_message})

self._message_history[room_id].append(
{
"role": "user",
"content": user_message,
"sender": msg.sender_name,
"sender_type": msg.sender_type,
}
)

total_messages = len(self._message_history[room_id])
messages = [dict(m) for m in self._message_history[room_id]]

total_messages = len(messages)
logger.info(
"Room %s: Processing with %s messages (first_msg=%s)",
room_id,
Expand Down
168 changes: 128 additions & 40 deletions src/thenvoi/converters/crewai.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

from __future__ import annotations

import json
import logging
from typing import Any

from thenvoi.core.protocols import HistoryConverter

logger = logging.getLogger(__name__)

# Type alias for CrewAI messages (simple dict format)
CrewAIMessages = list[dict[str, Any]]

Expand All @@ -14,56 +18,142 @@ class CrewAIHistoryConverter(HistoryConverter[CrewAIMessages]):
"""
Converts platform history to CrewAI-compatible message format.

Output: [{"role": "user", "content": "...", "sender": "..."}]
Output: [{"role": "user"|"assistant", "content": "...", "sender": "..."}]

Note:
- Only converts text messages (tool_call/tool_result events are skipped)
- User messages include sender name for context
- Other agents' messages are included with role "assistant"
- This agent's messages are skipped (redundant with CrewAI's internal state)
Notes:
- Text messages preserve sender context
- Other agents' text messages remain "assistant"
- This agent's own text messages are skipped (CrewAI already tracks them in-run)
- Tool events are converted to replayable text so CrewAI can see prior actions/results
"""

def __init__(self, agent_name: str = ""):
"""
Initialize converter.

Args:
agent_name: Name of this agent. Messages from this agent are skipped
(they're redundant with internal state). Messages from other
agents are included with their sender info.
"""
self._agent_name = agent_name

def set_agent_name(self, name: str) -> None:
"""
Set agent name so converter knows which messages to skip.

Args:
name: Name of this agent
"""
self._agent_name = name

@staticmethod
def _dump_json(value: Any) -> str:
return json.dumps(value, sort_keys=True, default=str)

@staticmethod
def _user_content(content: str, sender_name: str) -> str:
return f"[{sender_name}]: {content}" if sender_name else content

def _convert_tool_call(self, hist: dict[str, Any]) -> dict[str, Any] | None:
content = hist.get("content", "")
sender_name = hist.get("sender_name", self._agent_name)
sender_type = hist.get("sender_type", "Agent")

try:
event = json.loads(content)
except json.JSONDecodeError:
logger.warning("Failed to parse CrewAI tool_call: %s", repr(content[:100]))
return None

tool_name = event.get("name") or event.get("tool")
if not tool_name:
logger.warning(
"Skipping CrewAI tool_call with missing tool name: %s",
repr(content[:100]),
)
return None

tool_input = event.get("args")
if tool_input is None:
tool_input = event.get("input", {})

rendered = f"[Tool Call] {tool_name}"
if tool_input not in ({}, None, ""):
rendered = f"{rendered} {self._dump_json(tool_input)}"

return {
"role": "assistant",
"content": rendered,
"sender": sender_name,
"sender_type": sender_type,
}

def _convert_tool_result(self, hist: dict[str, Any]) -> dict[str, Any] | None:
content = hist.get("content", "")
sender_name = hist.get("sender_name", "")
sender_type = hist.get("sender_type", "System")

try:
event = json.loads(content)
except json.JSONDecodeError:
logger.warning(
"Failed to parse CrewAI tool_result: %s", repr(content[:100])
)
return None

tool_name = event.get("name") or event.get("tool")
if not tool_name:
logger.warning(
"Skipping CrewAI tool_result with missing tool name: %s",
repr(content[:100]),
)
return None

is_error = bool(event.get("is_error")) or "error" in event
output = event.get("output")
if output is None:
output = event.get("result")
if output is None and "error" in event:
output = event.get("error")
if output is None:
logger.warning(
"Skipping CrewAI tool_result with missing output/result: %s",
repr(content[:100]),
)
return None

rendered_output = output if isinstance(output, str) else self._dump_json(output)
prefix = "[Tool Error]" if is_error else "[Tool Result]"

return {
"role": "user",
"content": self._user_content(
f"{prefix} {tool_name}: {rendered_output}",
sender_name,
),
"sender": sender_name,
"sender_type": sender_type,
}

def convert(self, raw: list[dict[str, Any]]) -> CrewAIMessages:
"""Convert platform history to CrewAI format."""
messages: CrewAIMessages = []

for hist in raw:
message_type = hist.get("message_type", "text")

# Only convert text messages
if message_type != "text":
continue

role = hist.get("role", "user")
content = hist.get("content", "")
sender_name = hist.get("sender_name", "")
sender_type = hist.get("sender_type", "User")

if message_type == "thought":
continue

if message_type == "tool_call":
tool_call = self._convert_tool_call(hist)
if tool_call is not None:
messages.append(tool_call)
continue

if message_type == "tool_result":
tool_result = self._convert_tool_result(hist)
if tool_result is not None:
messages.append(tool_result)
continue

if message_type != "text":
continue

if role == "assistant" and sender_name == self._agent_name:
# Skip THIS agent's text (redundant with CrewAI state)
continue
elif role == "assistant":
# Other agents' messages
if role == "assistant":
messages.append(
{
"role": "assistant",
Expand All @@ -72,17 +162,15 @@ def convert(self, raw: list[dict[str, Any]]) -> CrewAIMessages:
"sender_type": sender_type,
}
)
else:
# User messages
messages.append(
{
"role": "user",
"content": f"[{sender_name}]: {content}"
if sender_name
else content,
"sender": sender_name,
"sender_type": sender_type,
}
)
continue

messages.append(
{
"role": "user",
"content": self._user_content(content, sender_name),
"sender": sender_name,
"sender_type": sender_type,
}
)

return messages
Loading
Loading