- Replaced direct instantiation of ChatOpenAI with a centralized get_llm function in CheckpointAgent, NoteAgent, ProjectAgent, and TaskAgent. - Introduced a new llm.py module to handle LLM model instantiation and API key management. - Updated settings.py to include LLM_MODEL and LLM_ROUTER_MODEL configurations. - Modified orchestrator.py to use get_router_llm for intent classification. - Updated requirements.txt to include litellm for LLM management. - Adjusted tests to mock get_llm instead of ChatOpenAI directly.
158 lines
4.5 KiB
Python
158 lines
4.5 KiB
Python
"""Project agent — full lifecycle management (list, get, create, update, archive, delete)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from langchain_core.messages import HumanMessage, SystemMessage
|
|
from langchain_core.tools import tool
|
|
|
|
from app.core.agent_registry import ChatAgent, registry
|
|
from app.core.llm import get_llm
|
|
|
|
_SYSTEM_PROMPT = (
|
|
"You are a project management assistant. You help users create, find,\n"
|
|
"update, and archive projects in their workspace.\n\n"
|
|
"Rules:\n"
|
|
" - status must be one of: active, archived\n"
|
|
" - client_id is optional; link to a client only when explicitly mentioned\n"
|
|
" - ai_summary is populated only when the user asks for a project summary;\n"
|
|
" derive it from context data — do not fabricate content\n"
|
|
" - Use list_projects for scoped queries; list_all_projects only when the\n"
|
|
" user wants a complete cross-client view including archived projects\n"
|
|
" - get_project requires a project UUID; resolve the ID first by calling\n"
|
|
" list_projects if you only have a project name\n"
|
|
" - Prefer archiving (update_project status=archived) over deletion;\n"
|
|
" only call delete_project when the user explicitly confirms deletion."
|
|
)
|
|
|
|
|
|
@tool
|
|
async def list_projects(
|
|
client_id: str = "",
|
|
include_archived: int = 0,
|
|
) -> str:
|
|
"""List projects, optionally filtered by client_id.
|
|
include_archived: 1 to include archived projects, 0 for active only (default).
|
|
"""
|
|
return json.dumps({
|
|
"action": "list",
|
|
"table": "projects",
|
|
"filters": {
|
|
"clientId": client_id or None,
|
|
"includeArchived": bool(include_archived),
|
|
},
|
|
})
|
|
|
|
|
|
@tool
|
|
async def list_all_projects() -> str:
|
|
"""List every project regardless of client or status.
|
|
Use only when the user wants a complete cross-client overview.
|
|
"""
|
|
return json.dumps({
|
|
"action": "list_all",
|
|
"table": "projects",
|
|
})
|
|
|
|
|
|
@tool
|
|
async def get_project(project_id: str) -> str:
|
|
"""Fetch a single project by its UUID."""
|
|
return json.dumps({
|
|
"action": "get",
|
|
"table": "projects",
|
|
"data": {"id": project_id},
|
|
})
|
|
|
|
|
|
@tool
|
|
async def create_project(
|
|
name: str,
|
|
client_id: str = "",
|
|
) -> str:
|
|
"""Create a new project.
|
|
name: human-readable project name (required)
|
|
client_id: optional UUID of the owning client
|
|
"""
|
|
return json.dumps({
|
|
"action": "create_record",
|
|
"table": "projects",
|
|
"data": {
|
|
"name": name,
|
|
"clientId": client_id or None,
|
|
},
|
|
})
|
|
|
|
|
|
@tool
|
|
async def update_project(
|
|
project_id: str,
|
|
name: str = "",
|
|
client_id: str = "",
|
|
status: str = "",
|
|
ai_summary: str = "",
|
|
) -> str:
|
|
"""Update a project. Only pass fields that should change.
|
|
project_id: UUID of the project (required)
|
|
status: active | archived
|
|
ai_summary: AI-generated summary text (populate only when explicitly requested)
|
|
"""
|
|
updates: dict[str, Any] = {}
|
|
if name:
|
|
updates["name"] = name
|
|
if client_id:
|
|
updates["clientId"] = client_id
|
|
if status:
|
|
updates["status"] = status
|
|
if ai_summary:
|
|
updates["aiSummary"] = ai_summary
|
|
return json.dumps({
|
|
"action": "update_record",
|
|
"table": "projects",
|
|
"data": {"id": project_id, "updates": updates},
|
|
})
|
|
|
|
|
|
@tool
|
|
async def delete_project(project_id: str) -> str:
|
|
"""Permanently delete a project and orphan its tasks.
|
|
IMPORTANT: prefer update_project(status='archived') unless the user
|
|
has explicitly confirmed they want permanent deletion.
|
|
"""
|
|
return json.dumps({
|
|
"action": "delete_record",
|
|
"table": "projects",
|
|
"data": {"id": project_id},
|
|
})
|
|
|
|
|
|
@registry.register
|
|
class ProjectAgent(ChatAgent):
|
|
def get_name(self) -> str:
|
|
return "project_agent"
|
|
|
|
def get_description(self) -> str:
|
|
return "Manages projects: list, get, create, update, archive, delete"
|
|
|
|
def get_tools(self) -> list[Any]:
|
|
return [
|
|
list_projects,
|
|
list_all_projects,
|
|
get_project,
|
|
create_project,
|
|
update_project,
|
|
delete_project,
|
|
]
|
|
|
|
async def handle(self, query: str, context: dict[str, Any]) -> str:
|
|
llm = get_llm()
|
|
messages = [
|
|
SystemMessage(content=_SYSTEM_PROMPT),
|
|
HumanMessage(
|
|
content=f"User query: {query}\nContext: {json.dumps(context)[:1000]}"
|
|
),
|
|
]
|
|
return await self._tool_loop(llm, messages, self.get_tools())
|