|
6 | 6 | from time import time |
7 | 7 | from typing import Any, Dict, List |
8 | 8 |
|
| 9 | +from langchain_core.callbacks import BaseCallbackHandler |
| 10 | +from langchain_anthropic import ChatAnthropic |
9 | 11 | from langchain_openai import ChatOpenAI |
10 | 12 |
|
11 | 13 | from app.config import Settings |
12 | 14 | 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 |
14 | 16 |
|
15 | 17 | logger = get_logger(__name__) |
16 | 18 |
|
17 | 19 |
|
| 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 | + |
18 | 47 | class BaseAgent(ABC): |
19 | 48 | """Abstract base class for all review agents.""" |
20 | 49 |
|
21 | 50 | def __init__(self, role: AgentRole, settings: Settings): |
22 | 51 | self.role = role |
23 | 52 | self.settings = settings |
24 | 53 | self.prompt_version = settings.default_prompt_version |
| 54 | + self.token_callback = TokenUsageCallback() |
25 | 55 | self.llm = self._create_llm(settings) |
26 | 56 |
|
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 | + ) |
29 | 66 | return ChatOpenAI( |
30 | 67 | api_key=settings.openai_api_key, |
31 | 68 | model=settings.openai_model, |
32 | 69 | temperature=settings.openai_temperature, |
33 | 70 | seed=settings.openai_seed, |
| 71 | + callbacks=[self.token_callback], |
34 | 72 | ) |
35 | 73 |
|
36 | 74 | @abstractmethod |
@@ -72,6 +110,15 @@ def _execute_with_timing(self, func, *args, **kwargs): |
72 | 110 | result = func(*args, **kwargs) |
73 | 111 | return result, time() - start |
74 | 112 |
|
| 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 | + |
75 | 122 | def _validate_findings(self, findings: List[Finding]) -> List[Finding]: |
76 | 123 | """Validate findings meet evidence requirements.""" |
77 | 124 | valid = [] |
|
0 commit comments