Coverage for src / osiris_cli / context.py: 0%
75 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-31 05:01 +0200
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-31 05:01 +0200
1import ast
2from pathlib import Path
3from typing import List, Dict, Any
4from rich.console import Console
5from .config import settings
7console = Console()
9class ContextManager:
11 def __init__(self):
12 self.loaded_files: Dict[str, str] = {}
13 self.image_files: List[str] = [] # Vision-Ready tracking
14 self.file_structures: Dict[str, Dict[str, Any]] = {}
16 def _estimate_tokens(self, text: str) -> int:
17 """Rough estimation: 4 chars per token."""
18 return len(text) // 4
20 def get_total_tokens(self) -> int:
21 return sum(self._estimate_tokens(content) for content in self.loaded_files.values())
23 def add_file(self, file_path: str) -> bool:
24 path = Path(file_path)
25 if not path.exists():
26 return False
28 if not path.is_file():
29 return False
31 try:
32 content = path.read_text(encoding='utf-8', errors='replace')
34 # Smart Context Truncation
35 limit = settings.max_context_chars
36 if len(content) > limit:
37 lines = content.splitlines()
38 total_lines = len(lines)
39 # Keep head and tail
40 head = lines[:50]
41 tail = lines[-20:]
43 truncated_content = "\n".join(head)
44 truncated_content += f"\n\n... [TRUNCATED: {total_lines - 70} lines hidden] ...\n"
45 truncated_content += f"... [File too large ({len(content)} chars > {limit}). Use read_file(path, start_line, end_line) to view missing parts.] ...\n\n"
46 truncated_content += "\n".join(tail)
48 self.loaded_files[str(path)] = truncated_content
49 else:
50 self.loaded_files[str(path)] = content
51 # Analyze structure immediately
52 self.file_structures[str(path)] = self.get_file_structure(str(path), content)
53 return True
54 except Exception as e:
55 return False
57 def remove_file(self, file_path: str):
58 if file_path in self.loaded_files:
59 del self.loaded_files[file_path]
60 if file_path in self.file_structures:
61 del self.file_structures[file_path]
63 def clear(self):
64 self.loaded_files = {}
65 self.file_structures = {}
67 def get_file_structure(self, path: str, content: str) -> Dict[str, Any]:
68 """Parse file structure (AST for Python, basic stats for others)."""
69 struct = {"classes": {}, "functions": [], "type": "unknown"}
71 if path.endswith(".py"):
72 struct["type"] = "python"
73 try:
74 tree = ast.parse(content)
75 for node in ast.walk(tree):
76 if isinstance(node, ast.ClassDef):
77 methods = [n.name for n in node.body if isinstance(n, ast.FunctionDef)]
78 struct["classes"][node.name] = methods
79 elif isinstance(node, ast.FunctionDef):
80 # Only top-level functions (approximate check: if parent is Module?)
81 # ast.walk is flat. We can iterate body for top-level.
82 pass
84 # Better top-level iteration
85 for node in tree.body:
86 if isinstance(node, ast.FunctionDef):
87 struct["functions"].append(node.name)
88 elif isinstance(node, ast.AsyncFunctionDef):
89 struct["functions"].append(node.name)
90 except Exception:
91 pass
92 return struct
94 def get_system_prompt_addition(self) -> str:
95 if not self.loaded_files:
96 return ""
98 text = "\n\n# CURRENT FILE CONTEXT\n"
99 for name, content in self.loaded_files.items():
100 text += f"## File: {name}\n```\n{content}\n```\n"
101 return text
103context = ContextManager()