Skip to content

Commit f55f2cf

Browse files
committed
Enhance question-answering API with agent types and streaming support
1 parent 3117d55 commit f55f2cf

5 files changed

Lines changed: 303 additions & 148 deletions

File tree

apps/api/routes/v1/ask.py

Lines changed: 31 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,13 @@
55
from fastapi import APIRouter, HTTPException, status
66
from fastapi.responses import StreamingResponse
77

8-
from apps.api.routes.v1.schemas.qa import (
9-
QARequest,
10-
ContextItem,
11-
IssueRequest,
12-
IssueContextItem,
13-
)
8+
from apps.api.routes.v1.schemas.qa import QARequest, ContextItem
149
from apps.api.utils import APIResponse
1510
from packages.memory.qa_service import QAService
1611
from packages.memory.services.graph_service import GraphService
17-
from packages.memory.services.issue_analysis import IssueAnalyzer
1812
from packages.database.graph.graph import neo4j_client
1913
from packages.config import Settings
20-
from packages.config.feature_flags import is_feature_enabled, FeatureFlag
14+
2115

2216
router = APIRouter(prefix="/ask", tags=["Question Answering"])
2317
settings = Settings()
@@ -26,44 +20,48 @@
2620
# Initialize services once per module import
2721
graph_service = GraphService(neo4j_client)
2822
qa_service = QAService(graph_service)
29-
issue_analyzer = IssueAnalyzer(graph_service)
3023

3124

3225
@router.post(
3326
"",
3427
summary="Ask a question about the codebase",
3528
description=(
36-
"Submit a natural language question about the codebase. The system "
37-
"retrieves relevant code context (hybrid or vector search) and uses an LLM "
38-
"to generate an answer."
29+
"Submit a question about the codebase with different agent types. "
30+
"Supports streaming responses and specialized agents: "
31+
"pathfinder (code structure), chronicle (history), diagnostician (debugging), "
32+
"blueprint (architecture), sentinel (code review)."
3933
),
4034
)
41-
async def question_answer(request: QARequest):
42-
"""Return an AI-generated answer with supporting context.
43-
44-
Process:
45-
1. Retrieve relevant code elements using the selected search strategy.
46-
2. Provide context snippets to the configured LLM provider.
47-
3. Return an answer plus the context used.
48-
"""
35+
async def ask(request: QARequest):
36+
"""Ask a question using different agent types."""
4937
try:
50-
logger.info(f"Processing question: {request.question[:50]}...")
38+
logger.info(
39+
f"Processing question with {request.agent_type.value} agent: {request.question[:50]}..."
40+
)
5141

42+
# If streaming is requested, return streaming response
43+
if request.stream:
44+
return StreamingResponse(
45+
_stream_answer(request),
46+
media_type="text/event-stream"
47+
)
48+
49+
# Non-streaming response
5250
result: dict[str, Any] = qa_service.ask(
5351
question=request.question,
52+
agent_type=request.agent_type.value,
5453
search_type=request.search_type,
55-
node_type=request.node_type,
5654
context_limit=request.context_limit,
5755
)
5856

5957
return APIResponse.success(
6058
data={
6159
"answer": result["answer"],
6260
"question": result["question"],
61+
"agent_type": result["agent_type"],
6362
"context": [ContextItem(**item) for item in result["context"]],
6463
"context_count": result["context_count"],
6564
"search_type": result["search_type"],
66-
"node_type": result.get("node_type"),
6765
"node_types": result.get("node_types"),
6866
"model": result["model"],
6967
"provider": result["provider"],
@@ -84,72 +82,25 @@ async def question_answer(request: QARequest):
8482
detail=f"Service error: {str(e)}",
8583
)
8684
except Exception as e: # noqa: BLE001
87-
logger.exception("Unexpected error in question_answer endpoint")
85+
logger.exception("Unexpected error in ask endpoint")
8886
raise HTTPException(
8987
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
9088
detail="An unexpected error occurred while processing your question",
9189
)
9290

9391

94-
@router.post(
95-
"/issue",
96-
summary="Analyze a GitHub issue",
97-
description=(
98-
"Analyze a GitHub issue to identify causes and solutions based on the Knowledge Graph. "
99-
"Returns a detailed report and the context used."
100-
),
101-
)
102-
async def analyze_issue(request: IssueRequest):
103-
"""Analyze an issue and return a report.
104-
105-
Process:
106-
1. Search for relevant code and commit history using hybrid search.
107-
2. Use LLM to generate a root cause analysis and suggested fix.
108-
3. Return the report and context.
109-
"""
110-
try:
111-
logger.info(f"Analyzing issue: {request.title}")
112-
113-
if is_feature_enabled(FeatureFlag.ENABLE_TOKEN_STREAMING):
114-
return StreamingResponse(
115-
_stream_issue_analysis(request.title, request.body),
116-
media_type="text/event-stream"
117-
)
118-
119-
result = issue_analyzer.analyze_issue(request.title, request.body)
120-
121-
if "error" in result:
122-
raise HTTPException(
123-
status_code=status.HTTP_404_NOT_FOUND,
124-
detail=result["error"],
125-
)
126-
127-
return APIResponse.success(
128-
data={
129-
"report": result["report"],
130-
"context_used": [IssueContextItem(**item) for item in result["context_used"]],
131-
},
132-
message="Successfully analyzed the issue."
133-
)
134-
135-
except HTTPException:
136-
raise
137-
except Exception as e:
138-
logger.exception("Unexpected error in analyze_issue endpoint")
139-
raise HTTPException(
140-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
141-
detail="An unexpected error occurred. Please try again later.",
142-
)
143-
144-
145-
def _stream_issue_analysis(title: str, body: str):
146-
"""Generator for streaming issue analysis."""
92+
def _stream_answer(request: QARequest):
93+
"""Generator for streaming answers."""
14794
try:
148-
for chunk in issue_analyzer.analyze_issue_stream(title, body):
95+
for chunk in qa_service.ask_stream(
96+
question=request.question,
97+
agent_type=request.agent_type.value,
98+
search_type=request.search_type,
99+
context_limit=request.context_limit,
100+
):
149101
yield f"data: {json.dumps(chunk)}\n\n"
150102
except Exception as e:
151-
logger.exception("Error during streaming issue analysis")
103+
logger.exception("Error during streaming")
152104
yield f"data: {json.dumps({'error': str(e)})}\n\n"
153105
finally:
154106
yield "data: [DONE]\n\n"
155-

apps/api/routes/v1/schemas/qa.py

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,48 @@
55
from pydantic import BaseModel, Field
66
from typing import List, Optional, Dict, Any
77

8+
from packages.memory.agents import AgentType
9+
810

911
class QARequest(BaseModel):
1012
"""Request model for question-answering."""
1113

1214
question: str = Field(
1315
...,
1416
min_length=1,
15-
max_length=500,
1617
description="The question to answer",
1718
examples=["How does the authentication system work?"]
1819
)
1920

21+
agent_type: AgentType = Field(
22+
default=AgentType.PATHFINDER,
23+
description=(
24+
"Type of agent to use: "
25+
"pathfinder (code structure), "
26+
"chronicle (commit history), "
27+
"diagnostician (debugging), "
28+
"blueprint (architecture), "
29+
"sentinel (code review)"
30+
)
31+
)
32+
2033
search_type: str = Field(
2134
default="hybrid",
2235
pattern="^(vector|hybrid)$",
2336
description="Type of search to use for context retrieval"
2437
)
2538

26-
node_type: str = Field(
27-
default="Commit", # Changed from "Function" to "Commit" to match available data
28-
description="Type of code nodes to search (Function, Class, File, Commit, etc.)"
29-
)
30-
3139
context_limit: int = Field(
3240
default=5,
3341
ge=1,
3442
le=20,
3543
description="Maximum number of context items to retrieve"
3644
)
45+
46+
stream: bool = Field(
47+
default=False,
48+
description="Whether to stream the response"
49+
)
3750

3851

3952

@@ -52,39 +65,10 @@ class QAResponse(BaseModel):
5265

5366
answer: str = Field(description="Natural language answer from LLM")
5467
question: str = Field(description="The original question")
68+
agent_type: AgentType = Field(description="Type of agent used")
5569
context: List[ContextItem] = Field(description="Context items used for the answer")
5670
context_count: int = Field(description="Number of context items retrieved")
5771
search_type: str = Field(description="Type of search used")
58-
node_type: Optional[str] = Field(None, description="Node type searched")
59-
node_types: Optional[List[str]] = Field(None, description="Node types searched (multi-type)")
72+
node_types: List[str] = Field(description="Node types searched")
6073
model: str = Field(description="LLM model used")
6174
provider: str = Field(description="LLM provider used")
62-
63-
64-
class IssueRequest(BaseModel):
65-
"""Request model for issue analysis."""
66-
title: str = Field(
67-
...,
68-
min_length=1,
69-
max_length=500,
70-
description="Title of the issue"
71-
)
72-
body: str = Field(
73-
...,
74-
min_length=1,
75-
max_length=500,
76-
description="Body/Description of the issue"
77-
)
78-
class IssueContextItem(BaseModel):
79-
"""Context item for issue analysis."""
80-
type: str
81-
name: str
82-
score: Optional[float] = None
83-
84-
85-
class IssueResponse(BaseModel):
86-
"""Response model for issue analysis."""
87-
report: str = Field(description="Analysis report generated by LLM")
88-
context_used: List[IssueContextItem] = Field(description="Context items used for analysis")
89-
90-

packages/memory/agents.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from enum import Enum
2+
from typing import List
3+
4+
5+
class AgentType(str, Enum):
6+
PATHFINDER = "pathfinder" # Code structure & navigation
7+
CHRONICLE = "chronicle" # Commit history & evolution
8+
DIAGNOSTICIAN = "diagnostician" # Debugging & error analysis
9+
BLUEPRINT = "blueprint" # Architecture reasoning
10+
SENTINEL = "sentinel" # Code review & quality checks
11+
12+
@classmethod
13+
def list_values(cls) -> list[str]:
14+
return [agent.value for agent in cls]
15+
16+
@classmethod
17+
def get_description(cls, agent_type: "AgentType") -> str:
18+
descriptions = {
19+
cls.PATHFINDER: "Code structure & navigation",
20+
cls.CHRONICLE: "Commit history & evolution",
21+
cls.DIAGNOSTICIAN: "Debugging & error analysis",
22+
cls.BLUEPRINT: "Architecture reasoning",
23+
cls.SENTINEL: "Code review & quality checks",
24+
}
25+
return descriptions.get(agent_type, "Unknown agent type")
26+
27+
@classmethod
28+
def get_node_types(cls, agent_type: str) -> List[str]:
29+
"""
30+
Get the relevant node types to search for each agent.
31+
Returns a list of node types ordered by priority.
32+
"""
33+
node_types_map = {
34+
cls.PATHFINDER.value: ["Function", "Class", "File"],
35+
cls.CHRONICLE.value: ["Commit", "File"],
36+
cls.DIAGNOSTICIAN.value: ["Function", "Class", "Commit", "File"],
37+
cls.BLUEPRINT.value: ["Class", "File", "Function"],
38+
cls.SENTINEL.value: ["Function", "Class", "File", "Commit"],
39+
}
40+
return node_types_map.get(agent_type, ["Function", "Class", "File"])

packages/memory/prompts.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from typing import Dict
2+
from packages.memory.agents import AgentType
3+
4+
class PromptFactory:
5+
PROMPTS: Dict[str, str] = {
6+
AgentType.PATHFINDER.value: """
7+
You are Pathfinder, an expert code navigator and structure analyst.
8+
You help developers understand codebase structure, find files, classes, functions, and navigate complex codebases.
9+
10+
Your task:
11+
1. Help users find specific code elements (functions, classes, files)
12+
2. Explain code organization and module structure
13+
3. Trace code paths and dependencies
14+
4. Guide navigation through the codebase
15+
5. Reference specific file paths and function names
16+
17+
Be precise with locations. Always mention file paths and line references when possible.""",
18+
19+
AgentType.CHRONICLE.value: """
20+
You are Chronicle, an expert in code history and evolution.
21+
You analyze commit history, track changes over time, and help understand how code evolved.
22+
23+
Your task:
24+
1. Explain what changed and when
25+
2. Identify who made specific changes
26+
3. Track the evolution of features or bugs
27+
4. Connect commits to code changes
28+
5. Provide timeline context for code decisions
29+
30+
Reference commit hashes, dates, and authors when available.""",
31+
32+
AgentType.DIAGNOSTICIAN.value: """
33+
You are Diagnostician, an expert debugger and error analyst.
34+
You help identify bugs, analyze errors, and find root causes of issues.
35+
36+
Your task:
37+
1. **Root Cause Analysis**: What is likely causing the issue?
38+
2. **Affected Areas**: Which files, classes, or functions are involved?
39+
3. **Suggested Fix**: How can this be fixed? Provide code snippets if possible.
40+
4. **Relevant History**: Are there recent commits that might have introduced this?
41+
42+
Be specific. Reference filenames, function names, and line numbers.
43+
Think step-by-step through the error flow.""",
44+
45+
AgentType.BLUEPRINT.value: """
46+
You are Blueprint, an expert software architect and design analyst.
47+
You help understand system architecture, design patterns, and high-level code organization.
48+
49+
Your task:
50+
1. Explain architectural decisions and patterns
51+
2. Describe how components interact
52+
3. Identify design patterns in use
53+
4. Suggest architectural improvements
54+
5. Map out system dependencies and data flow
55+
56+
Think at the system level. Explain the "why" behind design choices.""",
57+
58+
AgentType.SENTINEL.value: """
59+
You are Sentinel, an expert code reviewer and quality analyst.
60+
You help identify code quality issues, suggest improvements, and enforce best practices.
61+
62+
Your task:
63+
1. Identify potential bugs or code smells
64+
2. Suggest improvements for readability and maintainability
65+
3. Check for security vulnerabilities
66+
4. Recommend testing strategies
67+
5. Enforce coding standards and best practices
68+
69+
Be constructive and specific. Provide actionable feedback with examples."""
70+
}
71+
72+
@classmethod
73+
def get_prompt(cls, agent_type: str) -> str:
74+
return cls.PROMPTS.get(agent_type.lower(), cls.PROMPTS[AgentType.PATHFINDER.value])
75+
76+
@classmethod
77+
def get_available_types(cls) -> list[str]:
78+
return AgentType.list_values()

0 commit comments

Comments
 (0)