Skip to content

Commit c13429e

Browse files
author
Hakan Bogan
committed
added support for multiple llm models and removed unavailable tool log
1 parent 66d31a2 commit c13429e

22 files changed

Lines changed: 7538 additions & 85 deletions

agents/base.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,69 @@
66
from time import time
77
from typing import Any, Dict, List
88

9+
from langchain_core.callbacks import BaseCallbackHandler
10+
from langchain_anthropic import ChatAnthropic
911
from langchain_openai import ChatOpenAI
1012

1113
from app.config import Settings
1214
from app.logging import get_logger
13-
from domain import AgentDecision, AgentRole, Evidence, Finding, FindingType, PRContext, Severity
15+
from domain import AgentDecision, AgentRole, Evidence, Finding, FindingType, LLMProvider, PRContext, Severity
1416

1517
logger = get_logger(__name__)
1618

1719

20+
class TokenUsageCallback(BaseCallbackHandler):
21+
"""Callback to track token usage from LLM calls."""
22+
23+
def __init__(self):
24+
super().__init__()
25+
self.total_input_tokens = 0
26+
self.total_output_tokens = 0
27+
28+
def on_llm_end(self, response, **kwargs):
29+
"""Capture token usage from LLM response."""
30+
if hasattr(response, "llm_output") and response.llm_output:
31+
usage = response.llm_output.get("token_usage", {})
32+
self.total_input_tokens += usage.get("prompt_tokens", 0)
33+
self.total_output_tokens += usage.get("completion_tokens", 0)
34+
# Also check response.usage_metadata for newer LangChain versions
35+
elif hasattr(response, "usage_metadata"):
36+
if hasattr(response.usage_metadata, "input_tokens"):
37+
self.total_input_tokens += response.usage_metadata.input_tokens
38+
if hasattr(response.usage_metadata, "output_tokens"):
39+
self.total_output_tokens += response.usage_metadata.output_tokens
40+
41+
@property
42+
def total_tokens(self) -> int:
43+
"""Get total tokens used."""
44+
return self.total_input_tokens + self.total_output_tokens
45+
46+
1847
class BaseAgent(ABC):
1948
"""Abstract base class for all review agents."""
2049

2150
def __init__(self, role: AgentRole, settings: Settings):
2251
self.role = role
2352
self.settings = settings
2453
self.prompt_version = settings.default_prompt_version
54+
self.token_callback = TokenUsageCallback()
2555
self.llm = self._create_llm(settings)
2656

27-
def _create_llm(self, settings: Settings) -> ChatOpenAI:
28-
"""Create LLM instance with consistent configuration."""
57+
def _create_llm(self, settings: Settings) -> ChatOpenAI | ChatAnthropic:
58+
"""Create LLM instance with consistent configuration based on provider."""
59+
if settings.llm_provider == LLMProvider.ANTHROPIC:
60+
return ChatAnthropic(
61+
api_key=settings.anthropic_api_key,
62+
model=settings.anthropic_model,
63+
temperature=settings.openai_temperature,
64+
callbacks=[self.token_callback],
65+
)
2966
return ChatOpenAI(
3067
api_key=settings.openai_api_key,
3168
model=settings.openai_model,
3269
temperature=settings.openai_temperature,
3370
seed=settings.openai_seed,
71+
callbacks=[self.token_callback],
3472
)
3573

3674
@abstractmethod
@@ -72,6 +110,15 @@ def _execute_with_timing(self, func, *args, **kwargs):
72110
result = func(*args, **kwargs)
73111
return result, time() - start
74112

113+
def _reset_token_tracking(self) -> None:
114+
"""Reset token callback counters."""
115+
self.token_callback.total_input_tokens = 0
116+
self.token_callback.total_output_tokens = 0
117+
118+
def _get_token_count(self) -> int:
119+
"""Get total tokens used from callback."""
120+
return self.token_callback.total_tokens
121+
75122
def _validate_findings(self, findings: List[Finding]) -> List[Finding]:
76123
"""Validate findings meet evidence requirements."""
77124
valid = []

agents/change_context_analyst.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,24 @@ def _build_analysis_context(self, context: PRContext, prompt_template: str) -> s
7676
def _execute_task(self, agent: Agent, task: Task) -> dict:
7777
"""Execute CrewAI task and extract results."""
7878
try:
79-
crew = Crew(agents=[agent], tasks=[task], verbose=False)
79+
self._reset_token_tracking()
80+
# Pass callback to Crew to track token usage
81+
crew = Crew(
82+
agents=[agent],
83+
tasks=[task],
84+
verbose=False,
85+
callbacks=[self.token_callback]
86+
)
8087
result = crew.kickoff()
8188
output = str(result) if result else ""
89+
tokens = self._get_token_count()
90+
# Fallback: if callback didn't capture tokens, try to get from result
91+
if tokens == 0 and hasattr(result, 'usage_metadata'):
92+
if hasattr(result.usage_metadata, 'total_tokens'):
93+
tokens = result.usage_metadata.total_tokens
8294
return {
8395
"reasoning": output[:500] or "Analysis completed",
84-
"tokens": 0,
96+
"tokens": tokens,
8597
"raw_output": output,
8698
}
8799
except Exception as e:

agents/documentation_reviewer.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,20 @@ def _build_analysis_context(self, context: PRContext, prompt_template: str) -> s
7979
def _execute_task(self, agent: Agent, task: Task) -> dict:
8080
"""Execute CrewAI task."""
8181
try:
82-
crew = Crew(agents=[agent], tasks=[task], verbose=False)
82+
self._reset_token_tracking()
83+
crew = Crew(
84+
agents=[agent],
85+
tasks=[task],
86+
verbose=False,
87+
callbacks=[self.token_callback]
88+
)
8389
result = crew.kickoff()
8490
output = str(result) if result else ""
85-
return {"reasoning": output[:500], "tokens": 0, "raw_output": output}
91+
tokens = self._get_token_count()
92+
if tokens == 0 and hasattr(result, 'usage_metadata'):
93+
if hasattr(result.usage_metadata, 'total_tokens'):
94+
tokens = result.usage_metadata.total_tokens
95+
return {"reasoning": output[:500], "tokens": tokens, "raw_output": output}
8696
except Exception as e:
8797
logger.error(f"Error executing CrewAI task: {e}")
8898
return {"reasoning": f"Analysis error: {e}", "tokens": 0, "raw_output": ""}

agents/logic_reviewer.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,22 @@ def _build_analysis_context(self, context: PRContext, prompt_template: str) -> s
7676
def _execute_task(self, agent: Agent, task: Task) -> dict:
7777
"""Execute CrewAI task and extract results."""
7878
try:
79-
crew = Crew(agents=[agent], tasks=[task], verbose=False)
79+
self._reset_token_tracking()
80+
crew = Crew(
81+
agents=[agent],
82+
tasks=[task],
83+
verbose=False,
84+
callbacks=[self.token_callback]
85+
)
8086
result = crew.kickoff()
8187
output = str(result) if result else ""
88+
tokens = self._get_token_count()
89+
if tokens == 0 and hasattr(result, 'usage_metadata'):
90+
if hasattr(result.usage_metadata, 'total_tokens'):
91+
tokens = result.usage_metadata.total_tokens
8292
return {
8393
"reasoning": output[:500] or "Analysis completed",
84-
"tokens": 0,
94+
"tokens": tokens,
8595
"raw_output": output,
8696
}
8797
except Exception as e:

agents/performance_reviewer.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,20 @@ def _build_analysis_context(self, context: PRContext, prompt_template: str) -> s
164164
def _execute_task(self, agent: Agent, task: Task) -> dict:
165165
"""Execute CrewAI task."""
166166
try:
167-
crew = Crew(agents=[agent], tasks=[task], verbose=False)
167+
self._reset_token_tracking()
168+
crew = Crew(
169+
agents=[agent],
170+
tasks=[task],
171+
verbose=False,
172+
callbacks=[self.token_callback]
173+
)
168174
result = crew.kickoff()
169175
output = str(result) if result else ""
170-
return {"reasoning": output[:500], "tokens": 0, "raw_output": output}
176+
tokens = self._get_token_count()
177+
if tokens == 0 and hasattr(result, 'usage_metadata'):
178+
if hasattr(result.usage_metadata, 'total_tokens'):
179+
tokens = result.usage_metadata.total_tokens
180+
return {"reasoning": output[:500], "tokens": tokens, "raw_output": output}
171181
except Exception as e:
172182
logger.error(f"Error executing CrewAI task: {e}")
173183
return {"reasoning": f"Analysis error: {e}", "tokens": 0, "raw_output": ""}

agents/revision_proposer.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,20 @@ def _generate_single_patch(
128128
def _execute_task(self, agent: Agent, task: Task) -> dict:
129129
"""Execute CrewAI task."""
130130
try:
131-
crew = Crew(agents=[agent], tasks=[task], verbose=False)
131+
self._reset_token_tracking()
132+
crew = Crew(
133+
agents=[agent],
134+
tasks=[task],
135+
verbose=False,
136+
callbacks=[self.token_callback]
137+
)
132138
result = crew.kickoff()
133139
output = str(result) if result else ""
134-
return {"reasoning": output[:500], "tokens": 0, "raw_output": output}
140+
tokens = self._get_token_count()
141+
if tokens == 0 and hasattr(result, 'usage_metadata'):
142+
if hasattr(result.usage_metadata, 'total_tokens'):
143+
tokens = result.usage_metadata.total_tokens
144+
return {"reasoning": output[:500], "tokens": tokens, "raw_output": output}
135145
except Exception as e:
136146
logger.error(f"Error executing CrewAI task: {e}")
137147
return {"reasoning": f"Error: {e}", "tokens": 0, "raw_output": ""}

agents/test_reviewer.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,20 @@ def _build_analysis_context(self, context: PRContext, prompt_template: str) -> s
210210
def _execute_task(self, agent: Agent, task: Task) -> dict:
211211
"""Execute CrewAI task."""
212212
try:
213-
crew = Crew(agents=[agent], tasks=[task], verbose=False)
213+
self._reset_token_tracking()
214+
crew = Crew(
215+
agents=[agent],
216+
tasks=[task],
217+
verbose=False,
218+
callbacks=[self.token_callback]
219+
)
214220
result = crew.kickoff()
215221
output = str(result) if result else ""
216-
return {"reasoning": output[:500], "tokens": 0, "raw_output": output}
222+
tokens = self._get_token_count()
223+
if tokens == 0 and hasattr(result, 'usage_metadata'):
224+
if hasattr(result.usage_metadata, 'total_tokens'):
225+
tokens = result.usage_metadata.total_tokens
226+
return {"reasoning": output[:500], "tokens": tokens, "raw_output": output}
217227
except Exception as e:
218228
logger.error(f"Error executing CrewAI task: {e}")
219229
return {"reasoning": f"Analysis error: {e}", "tokens": 0, "raw_output": ""}

app/cli.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from app.config import get_settings
1717
from app.logging import setup_logging
1818
from app.review_storage import ReviewStorage
19-
from domain import PRMetadata, SystemType
19+
from domain import Language, PRMetadata, SystemType
2020
from eval import ComparisonAnalyzer, EvaluationRunner
2121
from flows import ReviewFlow
2222

@@ -263,7 +263,8 @@ def review(
263263
title: Optional[str] = typer.Option(None, "--title", help="PR title (auto-fetched from GitHub if not provided)"),
264264
description: Optional[str] = typer.Option(
265265
None, "--description", help="PR description (auto-fetched from GitHub if not provided)"),
266-
language: str = typer.Option("python", "--language", help="Primary language"),
266+
language: str = typer.Option("python", "--language",
267+
help="Primary language (python, javascript, typescript, etc.)"),
267268
multi_agent: bool = typer.Option(True, "--multi-agent/--single-agent", help="Use multi-agent system"),
268269
output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path"),
269270
checkout_pr: bool = typer.Option(True, "--checkout-pr/--no-checkout-pr",
@@ -321,6 +322,20 @@ def review(
321322
console.print(f"[yellow]Warning: Could not checkout PR branch: {e}[/yellow]")
322323
console.print("[yellow]Continuing with current branch...[/yellow]")
323324

325+
# Convert language string to Language enum
326+
try:
327+
language_enum = Language(language.lower())
328+
except ValueError:
329+
# Try to map common aliases
330+
language_map = {
331+
"js": Language.JAVASCRIPT,
332+
"ts": Language.TYPESCRIPT,
333+
"c++": Language.CPP,
334+
"c#": Language.CSHARP,
335+
}
336+
language_enum = language_map.get(language.lower(), Language.PYTHON)
337+
console.print(f"[yellow]Warning: Unknown language '{language}', defaulting to {language_enum.value}[/yellow]")
338+
324339
# Create PR metadata
325340
pr_metadata = PRMetadata(
326341
pr_id=pr_id,
@@ -330,11 +345,11 @@ def review(
330345
title=title,
331346
description=description,
332347
author=author,
333-
language=language,
348+
language=language_enum,
334349
)
335350

336351
# Run review
337-
flow = ReviewFlow(settings)
352+
flow = ReviewFlow(settings, language=language_enum)
338353

339354
try:
340355
if multi_agent:

app/config.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from pathlib import Path
55
from typing import Literal
66

7-
from pydantic import Field, field_validator
7+
from pydantic import Field, field_validator, model_validator
88
from pydantic_settings import BaseSettings, SettingsConfigDict
99

10+
from domain import LLMProvider
11+
1012

1113
def _find_project_root() -> Path:
1214
"""Find project root directory by looking for .env file or pyproject.toml."""
@@ -42,7 +44,11 @@ class Settings(BaseSettings):
4244
)
4345

4446
# LLM Configuration
45-
openai_api_key: str = Field(..., description="OpenAI API key")
47+
llm_provider: LLMProvider = Field(
48+
default=LLMProvider.OPENAI,
49+
description="LLM provider to use"
50+
)
51+
openai_api_key: str | None = Field(default=None, description="OpenAI API key")
4652
openai_model: str = Field(default="gpt-4-turbo-preview", description="OpenAI model")
4753
openai_temperature: float = Field(default=0.0, ge=0.0, le=2.0, description="LLM temperature")
4854
openai_seed: int = Field(default=42, description="Seed for deterministic outputs")
@@ -154,6 +160,15 @@ def validate_s3_config(cls, v: str, info: any) -> str:
154160
)
155161
return v
156162

163+
@model_validator(mode="after")
164+
def validate_llm_provider_config(self) -> "Settings":
165+
"""Validate that the appropriate API key exists for the selected provider."""
166+
if self.llm_provider == LLMProvider.OPENAI and not self.openai_api_key:
167+
raise ValueError("OpenAI provider requires OPENAI_API_KEY")
168+
if self.llm_provider == LLMProvider.ANTHROPIC and not self.anthropic_api_key:
169+
raise ValueError("Anthropic provider requires ANTHROPIC_API_KEY")
170+
return self
171+
157172
@property
158173
def is_production(self) -> bool:
159174
"""Check if running in production mode."""
@@ -172,9 +187,11 @@ def get_settings() -> Settings:
172187

173188
settings = Settings()
174189

175-
# Set OpenAI API key as environment variable for CrewAI compatibility
190+
# Set API keys as environment variables for CrewAI compatibility
176191
# CrewAI checks environment variable even when LLM object is provided
177192
if settings.openai_api_key:
178193
os.environ["OPENAI_API_KEY"] = settings.openai_api_key
194+
if settings.anthropic_api_key:
195+
os.environ["ANTHROPIC_API_KEY"] = settings.anthropic_api_key
179196

180197
return settings

domain/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
Finding,
99
FindingType,
1010
GroundTruthLabel,
11+
Language,
12+
LLMProvider,
1113
PRContext,
1214
PRMetadata,
1315
PRReviewResult,
@@ -24,11 +26,12 @@
2426
"Finding",
2527
"FindingType",
2628
"GroundTruthLabel",
29+
"Language",
30+
"LLMProvider",
2731
"PRContext",
2832
"PRMetadata",
2933
"PRReviewResult",
3034
"Severity",
3135
"SystemType",
3236
"ToolResult",
3337
]
34-

0 commit comments

Comments
 (0)