| | """Logger utility for LocalMate - Structured logging for debugging. |
| | |
| | Provides colored console logging with structured output for: |
| | - API request/response |
| | - Tool execution |
| | - LLM calls |
| | - Workflow tracing |
| | """ |
| |
|
| | import logging |
| | import json |
| | import sys |
| | from datetime import datetime |
| | from typing import Any |
| | from dataclasses import dataclass, field, asdict |
| |
|
| |
|
| | |
| | logging.basicConfig( |
| | level=logging.INFO, |
| | format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", |
| | datefmt="%H:%M:%S", |
| | stream=sys.stdout, |
| | ) |
| |
|
| | |
| | COLORS = { |
| | "RESET": "\033[0m", |
| | "BOLD": "\033[1m", |
| | "CYAN": "\033[36m", |
| | "GREEN": "\033[32m", |
| | "YELLOW": "\033[33m", |
| | "MAGENTA": "\033[35m", |
| | "BLUE": "\033[34m", |
| | "RED": "\033[31m", |
| | } |
| |
|
| |
|
| | def colorize(text: str, color: str) -> str: |
| | """Add color to text for terminal output.""" |
| | return f"{COLORS.get(color, '')}{text}{COLORS['RESET']}" |
| |
|
| |
|
| | class LocalMateLogger: |
| | """Structured logger for LocalMate with colored output.""" |
| | |
| | def __init__(self, name: str): |
| | self.logger = logging.getLogger(name) |
| | self.name = name |
| | |
| | def _format_data(self, data: Any, max_len: int = 500) -> str: |
| | """Format data for logging, truncating if needed.""" |
| | if data is None: |
| | return "None" |
| | |
| | if isinstance(data, (dict, list)): |
| | try: |
| | formatted = json.dumps(data, ensure_ascii=False, default=str) |
| | if len(formatted) > max_len: |
| | return formatted[:max_len] + "..." |
| | return formatted |
| | except: |
| | return str(data)[:max_len] |
| | |
| | text = str(data) |
| | return text[:max_len] + "..." if len(text) > max_len else text |
| | |
| | def api_request(self, endpoint: str, method: str, params: dict = None, body: Any = None): |
| | """Log API request.""" |
| | msg = f"{colorize('→ REQUEST', 'CYAN')} {colorize(method, 'BOLD')} {endpoint}" |
| | if params: |
| | msg += f"\n Params: {self._format_data(params)}" |
| | if body: |
| | msg += f"\n Body: {self._format_data(body)}" |
| | self.logger.info(msg) |
| | |
| | def api_response(self, endpoint: str, status: int, data: Any = None, duration_ms: float = None): |
| | """Log API response.""" |
| | status_color = "GREEN" if status < 400 else "RED" |
| | msg = f"{colorize('← RESPONSE', status_color)} {endpoint} [{status}]" |
| | if duration_ms: |
| | msg += f" ({duration_ms:.0f}ms)" |
| | if data: |
| | msg += f"\n Data: {self._format_data(data)}" |
| | self.logger.info(msg) |
| | |
| | def tool_call(self, tool_name: str, arguments: dict): |
| | """Log tool call start.""" |
| | msg = f"{colorize('🔧 TOOL', 'MAGENTA')} {colorize(tool_name, 'BOLD')}" |
| | msg += f"\n Args: {self._format_data(arguments)}" |
| | self.logger.info(msg) |
| | |
| | def tool_result(self, tool_name: str, result_count: int, sample: Any = None): |
| | """Log tool result.""" |
| | msg = f"{colorize('✓ RESULT', 'GREEN')} {tool_name} → {result_count} results" |
| | if sample: |
| | msg += f"\n Sample: {self._format_data(sample, max_len=200)}" |
| | self.logger.info(msg) |
| | |
| | def llm_call(self, provider: str, model: str, prompt_preview: str = None): |
| | """Log LLM call.""" |
| | msg = f"{colorize('🤖 LLM', 'BLUE')} {provider}/{model}" |
| | if prompt_preview: |
| | preview = prompt_preview[:100] + "..." if len(prompt_preview) > 100 else prompt_preview |
| | msg += f"\n Prompt: {preview}" |
| | self.logger.info(msg) |
| | |
| | def llm_response(self, provider: str, response_preview: str = None, tokens: int = None): |
| | """Log LLM response.""" |
| | msg = f"{colorize('💬 LLM RESPONSE', 'BLUE')} {provider}" |
| | if tokens: |
| | msg += f" ({tokens} tokens)" |
| | if response_preview: |
| | preview = response_preview[:150] + "..." if len(response_preview) > 150 else response_preview |
| | msg += f"\n Response: {preview}" |
| | self.logger.info(msg) |
| | |
| | def workflow_step(self, step: str, details: str = None): |
| | """Log workflow step.""" |
| | msg = f"{colorize('▶', 'YELLOW')} {step}" |
| | if details: |
| | msg += f": {details}" |
| | self.logger.info(msg) |
| | |
| | def error(self, message: str, error: Exception = None): |
| | """Log error.""" |
| | msg = f"{colorize('❌ ERROR', 'RED')} {message}" |
| | if error: |
| | msg += f"\n {type(error).__name__}: {str(error)}" |
| | self.logger.error(msg) |
| | |
| | def debug(self, message: str, data: Any = None): |
| | """Log debug info.""" |
| | msg = f"{colorize('DEBUG', 'CYAN')} {message}" |
| | if data: |
| | msg += f": {self._format_data(data)}" |
| | self.logger.debug(msg) |
| |
|
| |
|
| | @dataclass |
| | class WorkflowStep: |
| | """A step in the agent workflow.""" |
| | |
| | step_name: str |
| | tool_name: str | None = None |
| | purpose: str = "" |
| | input_summary: str = "" |
| | output_summary: str = "" |
| | result_count: int = 0 |
| | duration_ms: float = 0 |
| |
|
| |
|
| | @dataclass |
| | class AgentWorkflow: |
| | """Complete workflow trace for a chat request.""" |
| | |
| | query: str |
| | intent_detected: str = "" |
| | steps: list[WorkflowStep] = field(default_factory=list) |
| | total_duration_ms: float = 0 |
| | tools_used: list[str] = field(default_factory=list) |
| | |
| | def add_step(self, step: WorkflowStep): |
| | """Add a step to the workflow.""" |
| | self.steps.append(step) |
| | if step.tool_name and step.tool_name not in self.tools_used: |
| | self.tools_used.append(step.tool_name) |
| | |
| | def to_dict(self) -> dict: |
| | """Convert to dictionary for JSON serialization.""" |
| | return { |
| | "query": self.query, |
| | "intent_detected": self.intent_detected, |
| | "tools_used": self.tools_used, |
| | "steps": [ |
| | { |
| | "step": s.step_name, |
| | "tool": s.tool_name, |
| | "purpose": s.purpose, |
| | "results": s.result_count, |
| | } |
| | for s in self.steps |
| | ], |
| | "total_duration_ms": round(self.total_duration_ms, 1), |
| | } |
| | |
| | def to_summary(self) -> str: |
| | """Generate human-readable workflow summary.""" |
| | lines = [f"📊 **Workflow Summary**"] |
| | lines.append(f"- Query: \"{self.query[:50]}{'...' if len(self.query) > 50 else ''}\"") |
| | lines.append(f"- Intent: {self.intent_detected}") |
| | lines.append(f"- Tools: {', '.join(self.tools_used) or 'None'}") |
| | |
| | if self.steps: |
| | lines.append("\n**Steps:**") |
| | for i, step in enumerate(self.steps, 1): |
| | tool_info = f" ({step.tool_name})" if step.tool_name else "" |
| | results_info = f" → {step.result_count} results" if step.result_count else "" |
| | lines.append(f"{i}. {step.step_name}{tool_info}{results_info}") |
| | |
| | lines.append(f"\n⏱️ Total: {self.total_duration_ms:.0f}ms") |
| | return "\n".join(lines) |
| |
|
| |
|
| | |
| | agent_logger = LocalMateLogger("agent") |
| | api_logger = LocalMateLogger("api") |
| | tool_logger = LocalMateLogger("tools") |
| |
|