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
46 changes: 42 additions & 4 deletions src/backend/clara/agents/design_assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,13 @@ def _create_subagents(self) -> dict[str, AgentDefinition]:
"First call mcp__clara__get_prompt to get hydrated instructions. "
"This agent MUST use mcp__clara__ask to collect user input before building. "
"It should NOT build entities/agents until user confirms via ask tool responses. "
"Use mcp__clara__prompt_editor to show generated prompts for user editing."
"Use mcp__clara__prompt_editor to show generated prompts for user editing. "
"Use mcp__clara__get_agent_context to access uploaded context files."
),
tools=["mcp__clara__project", "mcp__clara__entity", "mcp__clara__agent",
"mcp__clara__ask", "mcp__clara__preview", "mcp__clara__phase",
"mcp__clara__get_prompt", "mcp__clara__prompt_editor"],
"mcp__clara__get_prompt", "mcp__clara__prompt_editor",
"mcp__clara__get_agent_context"],
prompt=phase3_prompt,
model="sonnet"
),
Expand Down Expand Up @@ -584,12 +586,48 @@ def __init__(self):
async def get_or_create_session(
self,
session_id: str,
project_id: str
project_id: str,
initial_blueprint_state: dict | None = None
) -> DesignAssistantSession:
"""Get an existing session or create a new one."""
"""Get an existing session or create a new one.

Args:
session_id: The session ID
project_id: The project ID
initial_blueprint_state: Optional blueprint state to initialize with
(used for add-agent mode to preserve existing agents)
"""
if session_id not in self._sessions:
session = DesignAssistantSession(session_id, project_id)
await session.start()

# If initial blueprint state provided, populate the tools state
if initial_blueprint_state:
tool_state = get_session_state(session_id)
tool_state["project"] = initial_blueprint_state.get("project")
tool_state["entities"] = initial_blueprint_state.get("entities", [])
tool_state["agents"] = initial_blueprint_state.get("agents", [])
tool_state["phase"] = DesignPhase.AGENT_CONFIGURATION.value

# Update session state to reflect the blueprint
if initial_blueprint_state.get("project"):
proj = initial_blueprint_state["project"]
session.state.blueprint_preview.project_name = proj.get("name")
session.state.blueprint_preview.project_type = proj.get("type")
session.state.inferred_domain = proj.get("domain")
session.state.blueprint_preview.agent_count = len(
initial_blueprint_state.get("agents", [])
)
session.state.blueprint_preview.entity_types = [
e.get("name") for e in initial_blueprint_state.get("entities", [])
]
session.state.phase = DesignPhase.AGENT_CONFIGURATION

logger.info(
f"Initialized session {session_id} with existing blueprint "
f"({len(initial_blueprint_state.get('agents', []))} agents)"
)

self._sessions[session_id] = session
return self._sessions[session_id]

Expand Down
36 changes: 29 additions & 7 deletions src/backend/clara/agents/simulation_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,23 @@ def extract_company_name_from_url(url: str) -> str:
return "the company"


def _get_anthropic_client() -> anthropic.Anthropic:
"""Get an Anthropic client using centralized configuration.

Uses settings.anthropic_api_key if configured, otherwise falls back
to the ANTHROPIC_API_KEY environment variable.

Raises:
ValueError: If no API key is available
"""
api_key = settings.anthropic_api_key
if api_key:
return anthropic.Anthropic(api_key=api_key)
# Let the client use ANTHROPIC_API_KEY env var (default behavior)
# This will raise AuthenticationError if not set
return anthropic.Anthropic()


async def search_company_products(url: str, role: str | None = None) -> str:
"""Use web search to gather information about a company's products.

Expand Down Expand Up @@ -535,7 +552,7 @@ async def search_company_products(url: str, role: str | None = None) -> str:
logger.info(f"Searching for company products: {search_query}")

try:
client = anthropic.Anthropic()
client = _get_anthropic_client()

# Build the research prompt
focus_item = ""
Expand All @@ -554,9 +571,9 @@ async def search_company_products(url: str, role: str | None = None) -> str:
Provide a detailed summary that would help someone understand what it's like \
to work at this company in a product/technical role."""

# Use Claude with web search tool
# Use Claude with web search tool (model from config)
response = client.messages.create(
model="claude-sonnet-4-20250514",
model=settings.web_search_model,
max_tokens=2048,
tools=[{"type": "web_search_20250305", "name": "web_search"}],
messages=[{"role": "user", "content": research_prompt}],
Expand All @@ -569,17 +586,22 @@ async def search_company_products(url: str, role: str | None = None) -> str:
result_text += block.text

if result_text:
logger.info(f"Successfully gathered company context via web search for {company_name}")
logger.info(
f"Successfully gathered company context via web search for {company_name}"
)
return result_text

return f"(Could not find detailed product information for {company_name})"

except anthropic.AuthenticationError as e:
logger.error(f"Anthropic authentication failed: {e}")
return "(Web search unavailable: API key not configured)"
except anthropic.APIError as e:
logger.warning(f"Anthropic API error during web search: {e}")
return f"(Web search unavailable: {e})"
return "(Web search temporarily unavailable)"
except Exception as e:
logger.warning(f"Error during company product search: {e}")
return f"(Could not search for company products: {e})"
logger.exception(f"Unexpected error during company product search: {type(e).__name__}")
return "(Could not search for company products)"


async def gather_company_context(url: str, role: str | None = None) -> str:
Expand Down
120 changes: 120 additions & 0 deletions src/backend/clara/agents/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,17 @@ def cleanup_stale_sessions() -> int:
"required": ["title", "prompt"],
}

GetAgentContextSchema = {
"type": "object",
"properties": {
"agent_index": {
"type": "integer",
"description": "Index of the agent to get context files for (0-based)",
},
},
"required": ["agent_index"],
}


def create_clara_tools(session_id: str):
"""Create MCP tools bound to a specific session.
Expand Down Expand Up @@ -508,12 +519,26 @@ async def preview_tool(args: dict) -> dict[str, Any]:
},
}

# Add context files info for each agent
agents = state.get("agents", [])
total_context_files = 0
for agent in agents:
context_files = agent.get("context_files", [])
if context_files:
blueprint["interview_agent"]["context_files"] = [
{"id": f.get("id"), "name": f.get("name"), "type": f.get("type")}
for f in context_files
]
total_context_files += len(context_files)

summary_parts = []
if project.get("name"):
summary_parts.append(f"Project: {project['name']}")
summary_parts.append(f"Knowledge Areas: {len(state['entities'])}")
if agent_caps.get("role"):
summary_parts.append(f"Interviewer: {agent_caps['role']}")
if total_context_files > 0:
summary_parts.append(f"Context Files: {total_context_files}")

blueprint_json = json.dumps(blueprint, indent=2)
summary = ", ".join(summary_parts)
Expand Down Expand Up @@ -810,6 +835,99 @@ async def prompt_editor_tool(args: dict) -> dict[str, Any]:
}]
}

@tool(
"get_agent_context",
"Get uploaded context files content for an interview agent",
GetAgentContextSchema
)
async def get_agent_context_tool(args: dict) -> dict[str, Any]:
"""Retrieve the extracted text from context files uploaded for an agent.

This allows the interview agent's system prompt to reference uploaded
documents like organization charts, process docs, or policy files.
"""
from sqlalchemy import select

from clara.db.models import AgentContextFile
from clara.db.session import async_session_maker

state = get_session_state(session_id)
agent_index = args["agent_index"]

# Get agents list and validate index
agents = state.get("agents", [])
if agent_index < 0 or agent_index >= len(agents):
msg = f"Error: Invalid agent index {agent_index}. "
msg += f"Only {len(agents)} agents configured."
return {
"content": [{"type": "text", "text": msg}],
"isError": True
}

agent = agents[agent_index]
agent_name = agent.get("name", f"Agent {agent_index}")
context_files_meta = agent.get("context_files", [])

if not context_files_meta:
return {
"content": [{
"type": "text",
"text": f"No context files uploaded for agent '{agent_name}'"
}]
}

# Fetch extracted text from database
file_ids = [f["id"] for f in context_files_meta if f.get("id")]

try:
async with async_session_maker() as db:
result = await db.execute(
select(AgentContextFile)
.where(AgentContextFile.id.in_(file_ids))
.where(AgentContextFile.deleted_at.is_(None))
)
files = result.scalars().all()

context_parts = []
for f in files:
if f.extracted_text and f.extraction_status == "success":
context_parts.append(
f"## {f.original_filename}\n\n{f.extracted_text}"
)
elif f.extraction_status == "partial":
context_parts.append(
f"## {f.original_filename} (truncated)\n\n{f.extracted_text}"
)
else:
context_parts.append(
f"## {f.original_filename}\n\n[Content could not be extracted]"
)

combined = "\n\n---\n\n".join(context_parts) if context_parts else ""

logger.info(
f"[{session_id}] Fetched {len(files)} context files "
f"for agent {agent_index}"
)

if combined:
result_text = f"Context files for agent '{agent_name}':"
result_text += f"\n\n{combined}"
else:
result_text = "No extractable content in uploaded files."

return {"content": [{"type": "text", "text": result_text}]}

except Exception as e:
logger.warning(f"[{session_id}] Failed to fetch context files: {e}")
return {
"content": [{
"type": "text",
"text": f"Error fetching context files: {str(e)}"
}],
"isError": True
}

# Create the MCP server with all tools
return create_sdk_mcp_server(
name="clara",
Expand All @@ -828,6 +946,7 @@ async def prompt_editor_tool(args: dict) -> dict[str, Any]:
hydrate_phase3_tool,
get_hydrated_prompt_tool,
prompt_editor_tool,
get_agent_context_tool,
],
)

Expand All @@ -847,4 +966,5 @@ async def prompt_editor_tool(args: dict) -> dict[str, Any]:
"mcp__clara__hydrate_phase3",
"mcp__clara__get_prompt",
"mcp__clara__prompt_editor",
"mcp__clara__get_agent_context",
]
Loading