|
|
import google.generativeai as genai |
|
|
import json |
|
|
import os |
|
|
from dotenv import load_dotenv |
|
|
|
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
|
|
|
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") |
|
|
if not GEMINI_API_KEY: |
|
|
raise ValueError( |
|
|
"GEMINI_API_KEY environment variable is required. " |
|
|
"Please set it in your .env file or environment." |
|
|
) |
|
|
|
|
|
|
|
|
genai.configure(api_key=GEMINI_API_KEY) |
|
|
|
|
|
|
|
|
from app.model_router import generate as router_generate, generate_with_info |
|
|
|
|
|
|
|
|
async def generate_documentation(task_title: str, what_i_did: str, code_snippet: str | None = None) -> dict: |
|
|
"""Generate docs for completed task. Returns {summary, details, tags}""" |
|
|
prompt = f""" |
|
|
Generate technical documentation for this completed work. |
|
|
|
|
|
Task: {task_title} |
|
|
What was done: {what_i_did} |
|
|
Code: {code_snippet or 'N/A'} |
|
|
|
|
|
Return ONLY valid JSON with: |
|
|
- "summary": one-line summary |
|
|
- "details": 2-3 paragraph technical documentation |
|
|
- "tags": array of 3-7 relevant tags |
|
|
|
|
|
Response must be pure JSON, no markdown. |
|
|
""" |
|
|
|
|
|
|
|
|
text = await router_generate(prompt, task_type="documentation") |
|
|
|
|
|
text = text.strip() |
|
|
if text.startswith("```"): |
|
|
text = text.split("```")[1] |
|
|
if text.startswith("json"): |
|
|
text = text[4:] |
|
|
return json.loads(text.strip()) |
|
|
|
|
|
|
|
|
async def synthesize_answer(context: str, query: str) -> str: |
|
|
"""Generate answer from context. Returns answer string.""" |
|
|
prompt = f""" |
|
|
Based on this project memory: |
|
|
{context} |
|
|
|
|
|
Answer: {query} |
|
|
|
|
|
Cite specific entries. If info not found, say so. |
|
|
""" |
|
|
|
|
|
|
|
|
return await router_generate(prompt, task_type="synthesis") |
|
|
|
|
|
|
|
|
async def get_embedding(text: str) -> list[float]: |
|
|
"""Get embedding vector for text using Gemini embedding model.""" |
|
|
result = genai.embed_content( |
|
|
model="models/text-embedding-004", |
|
|
content=text |
|
|
) |
|
|
return result['embedding'] |
|
|
|
|
|
|
|
|
async def generate_tasks(project_name: str, project_description: str, count: int = 50) -> list[dict]: |
|
|
"""Generate demo tasks for a project using LLM. |
|
|
|
|
|
Args: |
|
|
project_name: Name of the project |
|
|
project_description: Description of the project |
|
|
count: Number of tasks to generate (max 50) |
|
|
|
|
|
Returns: |
|
|
List of tasks with title and description |
|
|
""" |
|
|
|
|
|
count = min(count, 50) |
|
|
|
|
|
prompt = f""" |
|
|
You are a project manager creating demo tasks for a hackathon project. |
|
|
|
|
|
Project: {project_name} |
|
|
Description: {project_description} |
|
|
|
|
|
Generate exactly {count} simple, demo-friendly tasks for this software project. Each task should be: |
|
|
- Simple and quick to complete (5-30 minutes each) |
|
|
- Suitable for a demo or hackathon setting |
|
|
- Cover typical software development activities (setup, coding, testing, docs, UI) |
|
|
|
|
|
Include a mix of: |
|
|
- Setup tasks (environment, dependencies, config) |
|
|
- Feature implementation (simple features) |
|
|
- Bug fixes (minor issues) |
|
|
- Documentation (README, comments) |
|
|
- Testing (basic tests) |
|
|
- UI/UX improvements |
|
|
|
|
|
Return ONLY a valid JSON array with objects containing: |
|
|
- "title": short task title (max 100 chars) |
|
|
- "description": brief description (1 sentence) |
|
|
|
|
|
Example: |
|
|
[ |
|
|
{{"title": "Set up development environment", "description": "Install dependencies and configure local dev environment."}}, |
|
|
{{"title": "Add user login button", "description": "Create a login button component in the header."}} |
|
|
] |
|
|
|
|
|
Return ONLY the JSON array, no markdown or extra text. |
|
|
""" |
|
|
|
|
|
|
|
|
text = await router_generate(prompt, task_type="documentation") |
|
|
|
|
|
|
|
|
text = text.strip() |
|
|
if text.startswith("```"): |
|
|
lines = text.split("\n") |
|
|
|
|
|
text = "\n".join(lines[1:-1]) |
|
|
if text.startswith("json"): |
|
|
text = text[4:] |
|
|
|
|
|
try: |
|
|
tasks = json.loads(text.strip()) |
|
|
|
|
|
if not isinstance(tasks, list): |
|
|
raise ValueError("Response is not a list") |
|
|
|
|
|
|
|
|
validated_tasks = [] |
|
|
for task in tasks: |
|
|
if isinstance(task, dict) and "title" in task: |
|
|
validated_tasks.append({ |
|
|
"title": str(task.get("title", ""))[:100], |
|
|
"description": str(task.get("description", "")) |
|
|
}) |
|
|
|
|
|
return validated_tasks |
|
|
except json.JSONDecodeError as e: |
|
|
raise ValueError(f"Failed to parse LLM response as JSON: {e}") |
|
|
|
|
|
|
|
|
async def chat_with_tools(messages: list[dict], project_id: str) -> str: |
|
|
"""Chat with AI using MCP tools for function calling. |
|
|
|
|
|
Args: |
|
|
messages: List of chat messages [{'role': 'user/assistant', 'content': '...'}] |
|
|
project_id: Project ID for context |
|
|
|
|
|
Returns: |
|
|
AI response string |
|
|
""" |
|
|
from app.tools.projects import list_projects, create_project, join_project |
|
|
from app.tools.tasks import list_tasks, create_task, list_activity |
|
|
from app.tools.memory import complete_task, memory_search |
|
|
from app.model_router import router |
|
|
|
|
|
|
|
|
tools = [ |
|
|
{ |
|
|
"name": "list_projects", |
|
|
"description": "List all projects for a user", |
|
|
"parameters": { |
|
|
"type": "object", |
|
|
"properties": { |
|
|
"userId": {"type": "string", "description": "User ID"} |
|
|
}, |
|
|
"required": ["userId"] |
|
|
} |
|
|
}, |
|
|
{ |
|
|
"name": "list_tasks", |
|
|
"description": "List all tasks for a project", |
|
|
"parameters": { |
|
|
"type": "object", |
|
|
"properties": { |
|
|
"projectId": {"type": "string", "description": "Project ID"}, |
|
|
"status": {"type": "string", "enum": ["todo", "in_progress", "done"]} |
|
|
}, |
|
|
"required": ["projectId"] |
|
|
} |
|
|
}, |
|
|
{ |
|
|
"name": "list_activity", |
|
|
"description": "Get recent activity for a project", |
|
|
"parameters": { |
|
|
"type": "object", |
|
|
"properties": { |
|
|
"projectId": {"type": "string", "description": "Project ID"}, |
|
|
"limit": {"type": "number", "default": 20} |
|
|
}, |
|
|
"required": ["projectId"] |
|
|
} |
|
|
}, |
|
|
{ |
|
|
"name": "memory_search", |
|
|
"description": "Semantic search across project memory", |
|
|
"parameters": { |
|
|
"type": "object", |
|
|
"properties": { |
|
|
"projectId": {"type": "string", "description": "Project ID"}, |
|
|
"query": {"type": "string", "description": "Search query"} |
|
|
}, |
|
|
"required": ["projectId", "query"] |
|
|
} |
|
|
} |
|
|
] |
|
|
|
|
|
|
|
|
system_message = f""" |
|
|
You are an AI assistant helping users understand their project memory. |
|
|
Current Project ID: {project_id} |
|
|
|
|
|
You have access to these tools: |
|
|
- list_projects: List user's projects |
|
|
- list_tasks: List tasks in a project |
|
|
- list_activity: Get recent activity |
|
|
- memory_search: Search project memory semantically |
|
|
|
|
|
Use these tools to answer user questions accurately. |
|
|
""" |
|
|
|
|
|
|
|
|
chat_messages = [] |
|
|
for msg in messages: |
|
|
if msg["role"] == "system": |
|
|
system_message = msg["content"] |
|
|
else: |
|
|
chat_messages.append(msg) |
|
|
|
|
|
|
|
|
tool_prompt = f""" |
|
|
{system_message} |
|
|
|
|
|
To use tools, format your response as: |
|
|
TOOL: tool_name |
|
|
ARGS: {{"arg1": "value1"}} |
|
|
|
|
|
Available tools: |
|
|
{json.dumps(tools, indent=2)} |
|
|
""" |
|
|
|
|
|
|
|
|
full_messages = [{"role": "user", "content": tool_prompt}] + chat_messages |
|
|
|
|
|
|
|
|
chat_history = [] |
|
|
for msg in full_messages[:-1]: |
|
|
chat_history.append({ |
|
|
"role": "user" if msg["role"] == "user" else "model", |
|
|
"parts": [msg["content"]] |
|
|
}) |
|
|
|
|
|
|
|
|
model_name = router.get_model_for_task("chat") |
|
|
if not model_name: |
|
|
raise Exception("All models are rate limited. Please try again in a minute.") |
|
|
|
|
|
model = router.models[model_name] |
|
|
router._record_usage(model_name) |
|
|
|
|
|
|
|
|
chat = model.start_chat(history=chat_history) |
|
|
|
|
|
|
|
|
last_message = full_messages[-1]["content"] |
|
|
response = chat.send_message(last_message) |
|
|
|
|
|
|
|
|
response_text = response.text |
|
|
|
|
|
|
|
|
if "TOOL:" in response_text and "ARGS:" in response_text: |
|
|
|
|
|
lines = response_text.split("\n") |
|
|
tool_name = None |
|
|
args = None |
|
|
|
|
|
for line in lines: |
|
|
if line.startswith("TOOL:"): |
|
|
tool_name = line.replace("TOOL:", "").strip() |
|
|
elif line.startswith("ARGS:"): |
|
|
args = json.loads(line.replace("ARGS:", "").strip()) |
|
|
|
|
|
|
|
|
if tool_name and args: |
|
|
tool_result = None |
|
|
|
|
|
if tool_name == "list_projects": |
|
|
tool_result = list_projects(user_id=args["userId"]) |
|
|
elif tool_name == "list_tasks": |
|
|
tool_result = list_tasks( |
|
|
project_id=args["projectId"], |
|
|
status=args.get("status") |
|
|
) |
|
|
elif tool_name == "list_activity": |
|
|
tool_result = list_activity( |
|
|
project_id=args["projectId"], |
|
|
limit=args.get("limit", 20) |
|
|
) |
|
|
elif tool_name == "memory_search": |
|
|
tool_result = await memory_search( |
|
|
project_id=args["projectId"], |
|
|
query=args["query"] |
|
|
) |
|
|
|
|
|
|
|
|
if tool_result: |
|
|
follow_up = f"Tool {tool_name} returned: {json.dumps(tool_result)}\n\nBased on this, answer the user's question." |
|
|
final_response = chat.send_message(follow_up) |
|
|
return final_response.text |
|
|
|
|
|
return response_text |
|
|
|
|
|
|
|
|
async def task_chat( |
|
|
task_id: str, |
|
|
task_title: str, |
|
|
task_description: str, |
|
|
project_id: str, |
|
|
user_id: str, |
|
|
message: str, |
|
|
history: list[dict], |
|
|
current_datetime: str |
|
|
) -> dict: |
|
|
"""Chat with AI agent while working on a task. |
|
|
|
|
|
The agent can: |
|
|
- Answer questions and give coding advice |
|
|
- Search project memory for context |
|
|
- Complete the task when user indicates they're done |
|
|
|
|
|
Args: |
|
|
task_id: ID of the task being worked on |
|
|
task_title: Title of the task |
|
|
task_description: Description of the task |
|
|
project_id: Project ID |
|
|
user_id: User ID working on the task |
|
|
message: User's message |
|
|
history: Conversation history |
|
|
current_datetime: Current timestamp |
|
|
|
|
|
Returns: |
|
|
{message: str, taskCompleted?: bool, taskStatus?: str} |
|
|
""" |
|
|
from app.tools.memory import complete_task, memory_search |
|
|
from app.model_router import router |
|
|
|
|
|
|
|
|
system_prompt = f"""You are an AI assistant helping a developer work on a task. |
|
|
|
|
|
CURRENT TASK: |
|
|
- Title: {task_title} |
|
|
- Description: {task_description or 'No description'} |
|
|
- Task ID: {task_id} |
|
|
|
|
|
USER: {user_id} |
|
|
PROJECT: {project_id} |
|
|
CURRENT TIME: {current_datetime} |
|
|
|
|
|
YOUR CAPABILITIES: |
|
|
1. Answer questions and give coding advice related to the task |
|
|
2. Search project memory for relevant context (completed tasks, documentation) |
|
|
3. Complete the task when the user EXPLICITLY CONFIRMS |
|
|
|
|
|
TASK COMPLETION FLOW: |
|
|
When the user indicates they've finished (e.g., "I'm done", "finished it", describes what they did): |
|
|
1. Briefly acknowledge what they accomplished |
|
|
2. ASK SIMPLY: "Would you like me to mark this task as complete?" (just this question, nothing more) |
|
|
3. WAIT for user confirmation (e.g., "yes", "mark it", "complete it", "sure") |
|
|
4. ONLY after explicit confirmation, call the complete_task tool |
|
|
|
|
|
IMPORTANT: |
|
|
- Do NOT call complete_task until the user explicitly confirms |
|
|
- Do NOT ask for additional details or descriptions when confirming - just ask yes/no |
|
|
- The user has already told you what they did - use that information for the complete_task tool |
|
|
|
|
|
To use tools, format your response as: |
|
|
TOOL: tool_name |
|
|
ARGS: {{"arg1": "value1"}} |
|
|
RESULT_PENDING |
|
|
|
|
|
After I provide the tool result, give your final response to the user. |
|
|
|
|
|
Available tools: |
|
|
- memory_search: Search project memory. Args: {{"query": "search terms"}} |
|
|
- complete_task: Mark task as complete. Args: {{"what_i_did": "description of work done", "code_snippet": "optional code"}} |
|
|
|
|
|
Be helpful, concise, and focused on helping complete the task.""" |
|
|
|
|
|
|
|
|
chat_messages = [] |
|
|
|
|
|
|
|
|
for msg in history: |
|
|
role = "model" if msg["role"] == "assistant" else "user" |
|
|
chat_messages.append({ |
|
|
"role": role, |
|
|
"parts": [msg["content"]] |
|
|
}) |
|
|
|
|
|
|
|
|
model_name = router.get_model_for_task("chat") |
|
|
if not model_name: |
|
|
return {"message": "All AI models are temporarily unavailable. Please try again in a minute."} |
|
|
|
|
|
model = router.models[model_name] |
|
|
router._record_usage(model_name) |
|
|
|
|
|
|
|
|
first_message = f"{system_prompt}\n\nUser's first message will follow." |
|
|
chat_history = [{"role": "user", "parts": [first_message]}, {"role": "model", "parts": ["Understood. I'm ready to help you work on this task. What would you like to know or do?"]}] |
|
|
|
|
|
|
|
|
chat_history.extend(chat_messages) |
|
|
|
|
|
chat = model.start_chat(history=chat_history) |
|
|
|
|
|
|
|
|
response = chat.send_message(message) |
|
|
response_text = response.text |
|
|
|
|
|
|
|
|
task_completed = False |
|
|
task_status = "in_progress" |
|
|
|
|
|
if "TOOL:" in response_text and "ARGS:" in response_text: |
|
|
lines = response_text.split("\n") |
|
|
tool_name = None |
|
|
args = None |
|
|
|
|
|
for line in lines: |
|
|
if line.startswith("TOOL:"): |
|
|
tool_name = line.replace("TOOL:", "").strip() |
|
|
elif line.startswith("ARGS:"): |
|
|
try: |
|
|
args = json.loads(line.replace("ARGS:", "").strip()) |
|
|
except json.JSONDecodeError: |
|
|
continue |
|
|
|
|
|
if tool_name and args: |
|
|
tool_result = None |
|
|
|
|
|
if tool_name == "memory_search": |
|
|
tool_result = await memory_search( |
|
|
project_id=project_id, |
|
|
query=args.get("query", "") |
|
|
) |
|
|
elif tool_name == "complete_task": |
|
|
what_i_did = args.get("what_i_did", message) |
|
|
code_snippet = args.get("code_snippet") |
|
|
|
|
|
tool_result = await complete_task( |
|
|
task_id=task_id, |
|
|
project_id=project_id, |
|
|
user_id=user_id, |
|
|
what_i_did=what_i_did, |
|
|
code_snippet=code_snippet |
|
|
) |
|
|
|
|
|
if "error" not in tool_result: |
|
|
task_completed = True |
|
|
task_status = "done" |
|
|
|
|
|
|
|
|
if tool_result: |
|
|
follow_up = f"Tool {tool_name} returned: {json.dumps(tool_result)}\n\nProvide your response to the user." |
|
|
final_response = chat.send_message(follow_up) |
|
|
response_text = final_response.text |
|
|
|
|
|
return { |
|
|
"message": response_text, |
|
|
"taskCompleted": task_completed, |
|
|
"taskStatus": task_status |
|
|
} |
|
|
|