From 17f5c6e061b07d9ba7d1ef7929813f34c8c053ff Mon Sep 17 00:00:00 2001 From: Jean Silva Date: Tue, 17 Feb 2026 22:35:10 +0100 Subject: [PATCH 1/5] fix: add typing hints and cast for better static checks and clarity --- .../src/agentic_framework/mcp/provider.py | 2 +- .../tools/codebase_explorer.py | 133 ++++++++++++++++-- .../src/agentic_framework/tools/web_search.py | 7 +- 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/agentic-framework/src/agentic_framework/mcp/provider.py b/agentic-framework/src/agentic_framework/mcp/provider.py index 52d881d..a432a69 100644 --- a/agentic-framework/src/agentic_framework/mcp/provider.py +++ b/agentic-framework/src/agentic_framework/mcp/provider.py @@ -65,7 +65,7 @@ async def get_tools(self) -> List[Any]: return self._tools_cache @asynccontextmanager - async def tool_session(self, fail_fast: bool = True): + async def tool_session(self, fail_fast: bool = True) -> Any: """ Async context manager: load MCP tools with sessions that close on exit. diff --git a/agentic-framework/src/agentic_framework/tools/codebase_explorer.py b/agentic-framework/src/agentic_framework/tools/codebase_explorer.py index 4687770..f13ab0a 100644 --- a/agentic-framework/src/agentic_framework/tools/codebase_explorer.py +++ b/agentic-framework/src/agentic_framework/tools/codebase_explorer.py @@ -5,6 +5,97 @@ from agentic_framework.interfaces.base import Tool +# Language detection by file extension +LANGUAGE_EXTENSIONS: Dict[str, str] = { + ".py": "python", + ".pyi": "python", + ".js": "javascript", + ".mjs": "javascript", + ".cjs": "javascript", + ".ts": "typescript", + ".tsx": "typescript", + ".rs": "rust", + ".go": "go", + ".java": "java", + ".c": "c", + ".cpp": "cpp", + ".h": "c", + ".hpp": "cpp", + ".php": "php", +} + +# Regex patterns for each language +# Each pattern captures the relevant code construct +LANGUAGE_PATTERNS: Dict[str, List[str]] = { + "python": [ + r"^\s*(class\s+\w+.*?)[:\(]", + r"^\s*(async\s+def\s+\w+.*?)\(", + r"^\s*(def\s+\w+.*?)\(", + ], + "javascript": [ + r"^\s*(export\s+)?(default\s+)?(class\s+\w+)", + r"^\s*(export\s+)?(default\s+)?(function\s*\*?\s+\w+)", + r"^\s*(export\s+)?(const\s+\w+\s*=\s*\([^)]*\)\s*=>)", + r"^\s*(export\s+)?(const\s+\w+\s*=\s*async\s*\([^)]*\)\s*=>)", + ], + "typescript": [ + r"^\s*(export\s+)?(default\s+)?(class\s+\w+)", + r"^\s*(export\s+)?(default\s+)?(function\s*\*?\s+\w+)", + r"^\s*(export\s+)?(const\s+\w+\s*=\s*\([^)]*\)\s*=>)", + r"^\s*(export\s+)?(const\s+\w+\s*=\s*async\s*\([^)]*\)\s*=>)", + r"^\s*(export\s+)?(interface\s+\w+)", + r"^\s*(export\s+)?(type\s+\w+)", + r"^\s*(export\s+)?(enum\s+\w+)", + r"^\s*(export\s+)?(abstract\s+class\s+\w+)", + r"^\s*(export\s+)?(namespace\s+\w+)", + ], + "rust": [ + r"^\s*(pub\s+(\([^)]+\)\s+)?)?(struct\s+\w+)", + r"^\s*(pub\s+)?(enum\s+\w+)", + r"^\s*(pub\s+)?(trait\s+\w+)", + r"^\s*(pub\s+(\([^)]+\)\s+)?)?(async\s+)?(fn\s+\w+)", + r"^\s*(impl\s+(<[^>]+>\s+)?\w+)", + r"^\s*(pub\s+)?(mod\s+\w+)", + ], + "go": [ + r"^\s*func\s+\(\w+\s+\*?\w+\)\s*\w+", # method with receiver + r"^\s*func\s+\w+", # function + r"^\s*type\s+\w+\s+struct\b", + r"^\s*type\s+\w+\s+interface\b", + r"^\s*type\s+\w+\s+func\b", + r"^\s*type\s+\w+\s*\(", # type alias with params + ], + "java": [ + r"^\s*(public|private|protected)?\s*(abstract\s+)?(class\s+\w+)", + r"^\s*(public\s+)?(interface\s+\w+)", + r"^\s*(public\s+)?(enum\s+\w+)", + r"^\s*@\w+(\([^)]*\))?\s*$", # annotations (standalone) + r"^\s*(public|private|protected)\s+(static\s+)?[\w<>?,\s]+\s+\w+\s*\(", # methods + r"^\s*(public|private|protected)\s+\w+\s*\([^\)]*\)\s*\{", # constructors + ], + "c": [ + r"^\s*(typedef\s+)?(struct\s+\w*)", + r"^\s*(typedef\s+)?(enum\s+\w*)", + r"^\s*(typedef\s+)?(union\s+\w*)", + r"^\s*(void|int|char|float|double|long|unsigned|signed|short)\s+[\w\s\*]+\s*\w+\s*\(", + ], + "cpp": [ + r"^\s*(typedef\s+)?(struct\s+\w*)", + r"^\s*(typedef\s+)?(enum\s+\w*)", + r"^\s*(typedef\s+)?(union\s+\w*)", + r"^\s*(class\s+\w+)", + r"^\s*(namespace\s+\w+)", + r"^\s*(template\s*<[^>]*>)?\s*class\s+\w+", # template class + r"^\s*(template\s*<[^>]*>)?\s*[\w:]+\s+[\w:]+\s*\(", # template function + ], + "php": [ + r"^\s*(abstract\s+)?(final\s+)?(class\s+\w+)", + r"^\s*(interface\s+\w+)", + r"^\s*(trait\s+\w+)", + r"^\s*(public|private|protected)?\s*(static\s+)?function\s+\w+", + ], +} + class CodebaseExplorer: """ @@ -77,7 +168,7 @@ def _build_tree(self, current_dir: Path, depth: int, max_depth: int) -> Dict[str class FileOutlinerTool(CodebaseExplorer, Tool): - """Tool to extract high-level signatures from a file.""" + """Tool to extract high-level signatures from various programming language files.""" @property def name(self) -> str: @@ -85,25 +176,47 @@ def name(self) -> str: @property def description(self) -> str: - return "Extracts classes and functions signatures from a Python file to skim its content." + return """Extracts classes, functions, and other definitions from code files. + Supports: Python, JavaScript, TypeScript, Rust, Go, Java, C/C++, PHP. + Returns a list of signatures with their line numbers. + Output format: [{"line": 15, "signature": "class MyAgent:"}, ...]""" def invoke(self, file_path: str) -> Any: full_path = self.root_dir / file_path if not full_path.exists() or not full_path.is_file(): return f"Error: File {file_path} not found." - outline = [] - pattern = re.compile(r"^\s*(class\s+\w+|async\s+def\s+\w+|def\s+\w+)") + language = self._detect_language(file_path) + if language is None: + return f"Error: Unsupported file type for {file_path}. Supported: Python, JS/TS, Rust, Go, Java, C/C++, PHP" + + patterns = LANGUAGE_PATTERNS.get(language, []) + return self._extract_outline(full_path, patterns) + + def _detect_language(self, file_path: str) -> Optional[str]: + """Detect programming language from file extension.""" + ext = Path(file_path).suffix.lower() + return LANGUAGE_EXTENSIONS.get(ext) + + def _extract_outline(self, file_path: Path, patterns: List[str]) -> List[Dict[str, Any]]: + """Extract code outline using regex patterns.""" + outline: List[Dict[str, Any]] = [] + compiled_patterns = [re.compile(p) for p in patterns] try: - with open(full_path, "r", encoding="utf-8") as f: - for line in f: - match = pattern.match(line) - if match: - outline.append(line.strip()) + with open(file_path, "r", encoding="utf-8", errors="replace") as f: + for line_num, line in enumerate(f, 1): + for pattern in compiled_patterns: + if pattern.search(line): + # Clean up the signature (limit length for readability) + signature = line.strip() + if len(signature) > 100: + signature = signature[:97] + "..." + outline.append({"line": line_num, "signature": signature}) + break # Only match first pattern per line return outline except Exception as e: - return f"Error reading file: {e}" + return [{"error": f"Error reading file: {e}"}] class FileFragmentReaderTool(CodebaseExplorer, Tool): diff --git a/agentic-framework/src/agentic_framework/tools/web_search.py b/agentic-framework/src/agentic_framework/tools/web_search.py index 1401dca..5d5aabc 100644 --- a/agentic-framework/src/agentic_framework/tools/web_search.py +++ b/agentic-framework/src/agentic_framework/tools/web_search.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any, Dict, cast from tavily import TavilyClient # type: ignore[import-untyped] @@ -8,7 +8,7 @@ class WebSearchTool(Tool): """WebSearch Tool that uses Tavily API.""" - def __init__(self): + def __init__(self) -> None: self.tavily_client = TavilyClient() @property @@ -21,4 +21,5 @@ def description(self) -> str: def invoke(self, query: str) -> Dict[str, Any]: """Search the web for information""" - return self.tavily_client.search(query) + result = self.tavily_client.search(query) + return cast(Dict[str, Any], result) From 4ab90183792db0be8c55162b01a658459ef9771f Mon Sep 17 00:00:00 2001 From: Jean Silva Date: Tue, 17 Feb 2026 22:38:09 +0100 Subject: [PATCH 2/5] feat(core): add typing to SimpleAgent init and expand code explorer - annotate SimpleAgent.__init__ with Any and -> None to improve type clarity and mypy compatibility; preserve default model and temp. - expand AGENTS.md with an Environment section describing local aliases and package manager (uv) so contributors understand dev assumptions. - document multi-language support for get_file_outline and list supported languages; add example return format for callers. - add comprehensive tests for codebase explorer tools: - language detection mapping for many extensions - FileOutlinerTool tests for Python outlines - fixtures for temporary directories and outliner setup These changes improve type safety, developer onboarding docs, and increase test coverage for multi-language file outlining. --- AGENTS.md | 13 +- README.md | 4 +- .../agentic_framework/core/developer_agent.py | 7 +- .../agentic_framework/core/simple_agent.py | 2 +- .../tests/test_codebase_explorer.py | 580 ++++++++++++++++++ 5 files changed, 600 insertions(+), 6 deletions(-) create mode 100644 agentic-framework/tests/test_codebase_explorer.py diff --git a/AGENTS.md b/AGENTS.md index 99ea0f6..ec93a00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,12 @@ This guide is for both external LLMs working within this codebase and developers building new agents and tools. +## Environment + +We use uv for package management and running commands. +cd is aliased to z (zoxide), use full path instead. +ls is aliased to eza, use full path instead. + ## Project Overview This is a **LangChain + MCP framework** for building agentic systems in Python 3.12+. @@ -135,11 +141,14 @@ Orchestrates 3 specialist agents (flight, city intel, reviewer) with MCP tools. Codebase exploration agent with specialized local tools and webfetch MCP: - `find_files` - Fast file search via fd - `discover_structure` - Directory tree exploration -- `get_file_outline` - Extract class/function signatures +- `get_file_outline` - Extract class/function signatures (multi-language) - `read_file_fragment` - Read specific line ranges - `code_search` - Fast pattern search via ripgrep - `webfetch` (MCP) - Web content fetching +**Supported Languages for `get_file_outline`**: +Python, JavaScript, TypeScript, Rust, Go, Java, C/C++, PHP + ## Using Existing Agents ### CLI Usage @@ -313,6 +322,8 @@ __all__ = [ - Use `find_files` when you need to locate files by name/pattern - Use `discover_structure` for project layout overview - Use `get_file_outline` to skim file contents before reading + - Supports: Python (`.py`), JavaScript (`.js`), TypeScript (`.ts`), Rust (`.rs`), Go (`.go`), Java (`.java`), C (`.c`, `.h`), C++ (`.cpp`, `.hpp`), PHP (`.php`) + - Returns: `[{"line": 15, "signature": "class MyAgent:"}, ...]` - Use `read_file_fragment` to read specific lines (format: `path:start:end`) - Use `code_search` for fast global pattern matching diff --git a/README.md b/README.md index 16f936d..c011229 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,9 @@ tail -f agentic-framework/logs/agent.log The `developer` agent is designed to assist with codebase maintenance and understanding. It comes equipped with local tools for: - **File Discovery**: Finding files by name across the project. - **Structure Exploration**: Visualizing the project directory tree. -- **Code Outlining**: Extracting functions and classes from Python files. +- **Code Outlining**: Extracting functions, classes, and signatures from code files. + - Supports: Python, JavaScript, TypeScript, Rust, Go, Java, C/C++, PHP + - Returns line numbers for precise navigation. - **Pattern Search**: Global search using `ripgrep` for fast pattern matching. Implementation: `src/agentic_framework/core/developer_agent.py` diff --git a/agentic-framework/src/agentic_framework/core/developer_agent.py b/agentic-framework/src/agentic_framework/core/developer_agent.py index afc53f1..cae3bb0 100644 --- a/agentic-framework/src/agentic_framework/core/developer_agent.py +++ b/agentic-framework/src/agentic_framework/core/developer_agent.py @@ -27,16 +27,17 @@ def system_prompt(self) -> str: Your goal is to help the user understand and maintain their codebase. You have access to several specialized tools for: 1. Discovering the project structure and finding files by name (`find_files`). - 2. Extracting class and function outlines from Python files (`get_file_outline`). + 2. Extracting class and function outlines from code files (`get_file_outline`). + Supports: Python, JavaScript, TypeScript, Rust, Go, Java, C/C++, PHP. 3. Reading specific fragments of a file (`read_file_fragment`). 4. Searching the codebase for patterns using ripgrep (`code_search`). - + When you need to find a specific file by name, use `find_files`. When asked about the project structure, start with `discover_structure`. When asked to explain a file, start with `get_file_outline` to get an overview. Use `read_file_fragment` to read specific lines if you need more detail. Use `code_search` for fast global pattern matching. - + Always provide clear, concise explanations and suggest improvements when relevant. You also have access to MCP tools like `webfetch` if you need to fetch information from the web. """ diff --git a/agentic-framework/src/agentic_framework/core/simple_agent.py b/agentic-framework/src/agentic_framework/core/simple_agent.py index dbdcc06..3c2d59f 100644 --- a/agentic-framework/src/agentic_framework/core/simple_agent.py +++ b/agentic-framework/src/agentic_framework/core/simple_agent.py @@ -15,7 +15,7 @@ class SimpleAgent(Agent): No MCP access (mcp_servers=None in registry). """ - def __init__(self, model_name: str = "gpt-5-nano", temperature: float = 0.0, **kwargs): + def __init__(self, model_name: str = "gpt-5-nano", temperature: float = 0.0, **kwargs: Any) -> None: self.model = ChatOpenAI(model=model_name, temperature=temperature) self.prompt = ChatPromptTemplate.from_messages( [("system", "You are a helpful assistant."), ("user", "{input}")] diff --git a/agentic-framework/tests/test_codebase_explorer.py b/agentic-framework/tests/test_codebase_explorer.py new file mode 100644 index 0000000..4a9a6b8 --- /dev/null +++ b/agentic-framework/tests/test_codebase_explorer.py @@ -0,0 +1,580 @@ +"""Tests for codebase explorer tools.""" + +import tempfile +from pathlib import Path + +import pytest + +from agentic_framework.tools.codebase_explorer import ( + LANGUAGE_EXTENSIONS, + LANGUAGE_PATTERNS, + FileFragmentReaderTool, + FileOutlinerTool, + StructureExplorerTool, +) + + +class TestLanguageDetection: + """Tests for language detection by file extension.""" + + def test_python_extensions(self): + assert LANGUAGE_EXTENSIONS[".py"] == "python" + assert LANGUAGE_EXTENSIONS[".pyi"] == "python" + + def test_javascript_extensions(self): + assert LANGUAGE_EXTENSIONS[".js"] == "javascript" + assert LANGUAGE_EXTENSIONS[".mjs"] == "javascript" + assert LANGUAGE_EXTENSIONS[".cjs"] == "javascript" + + def test_typescript_extensions(self): + assert LANGUAGE_EXTENSIONS[".ts"] == "typescript" + assert LANGUAGE_EXTENSIONS[".tsx"] == "typescript" + + def test_rust_extension(self): + assert LANGUAGE_EXTENSIONS[".rs"] == "rust" + + def test_go_extension(self): + assert LANGUAGE_EXTENSIONS[".go"] == "go" + + def test_java_extension(self): + assert LANGUAGE_EXTENSIONS[".java"] == "java" + + def test_c_cpp_extensions(self): + assert LANGUAGE_EXTENSIONS[".c"] == "c" + assert LANGUAGE_EXTENSIONS[".h"] == "c" + assert LANGUAGE_EXTENSIONS[".cpp"] == "cpp" + assert LANGUAGE_EXTENSIONS[".hpp"] == "cpp" + + def test_php_extension(self): + assert LANGUAGE_EXTENSIONS[".php"] == "php" + + +class TestFileOutlinerTool: + """Tests for FileOutlinerTool with multi-language support.""" + + @pytest.fixture + def temp_dir(self): + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + @pytest.fixture + def outliner(self, temp_dir): + return FileOutlinerTool(str(temp_dir)) + + def test_python_outline(self, outliner, temp_dir): + """Test Python file outline extraction.""" + code = ''' +import os + +class MyAgent: + """A simple agent.""" + + def __init__(self): + pass + + async def run(self, input_data): + return "done" + +def helper_function(): + pass +''' + (temp_dir / "test.py").write_text(code) + result = outliner.invoke("test.py") + + assert isinstance(result, list) + assert len(result) == 4 + + signatures = [item["signature"] for item in result] + lines = [item["line"] for item in result] + + assert any("class MyAgent" in s for s in signatures) + assert any("def __init__" in s for s in signatures) + assert any("async def run" in s for s in signatures) + assert any("def helper_function" in s for s in signatures) + + # Verify line numbers + assert 4 in lines # class + assert 7 in lines # def __init__ + assert 10 in lines # async def run + + def test_javascript_outline(self, outliner, temp_dir): + """Test JavaScript file outline extraction.""" + code = """ +export class UserService { + constructor() {} + + async fetchUser(id) { + return fetch(`/users/${id}`); + } +} + +export function helper() { + return "helper"; +} + +const processItem = (item) => { + return item; +}; + +const asyncProcess = async (data) => { + return data; +}; +""" + (temp_dir / "test.js").write_text(code) + result = outliner.invoke("test.js") + + assert isinstance(result, list) + signatures = [item["signature"] for item in result] + + assert any("class UserService" in s for s in signatures) + assert any("function helper" in s for s in signatures) + assert any("processItem" in s for s in signatures) + assert any("asyncProcess" in s for s in signatures) + + def test_typescript_outline(self, outliner, temp_dir): + """Test TypeScript file outline extraction.""" + code = """ +interface User { + id: number; + name: string; +} + +type UserRole = "admin" | "user"; + +export class UserService { + async getUser(id: number): Promise { + return {} as User; + } +} + +export enum Status { + Active, + Inactive +} + +abstract class BaseService { + abstract connect(): void; +} +""" + (temp_dir / "test.ts").write_text(code) + result = outliner.invoke("test.ts") + + assert isinstance(result, list) + signatures = [item["signature"] for item in result] + + assert any("interface User" in s for s in signatures) + assert any("type UserRole" in s for s in signatures) + assert any("class UserService" in s for s in signatures) + assert any("enum Status" in s for s in signatures) + assert any("abstract class BaseService" in s for s in signatures) + + def test_rust_outline(self, outliner, temp_dir): + """Test Rust file outline extraction.""" + code = """ +pub struct User { + pub name: String, +} + +struct PrivateData { + id: u64, +} + +pub enum Status { + Active, + Inactive, +} + +pub trait Repository { + fn find(&self, id: u64) -> Option; +} + +impl User { + pub fn new(name: String) -> Self { + Self { name } + } +} + +pub fn create_user(name: &str) -> User { + User { name: name.to_string() } +} + +pub async fn fetch_user(id: u64) -> Result { + Ok(User { name: "test".into() }) +} + +pub mod models { + pub struct Model {} +} +""" + (temp_dir / "test.rs").write_text(code) + result = outliner.invoke("test.rs") + + assert isinstance(result, list) + signatures = [item["signature"] for item in result] + + assert any("struct User" in s for s in signatures) + assert any("struct PrivateData" in s for s in signatures) + assert any("enum Status" in s for s in signatures) + assert any("trait Repository" in s for s in signatures) + assert any("impl User" in s for s in signatures) + assert any("fn create_user" in s for s in signatures) + assert any("fn fetch_user" in s for s in signatures) + assert any("mod models" in s for s in signatures) + + def test_go_outline(self, outliner, temp_dir): + """Test Go file outline extraction.""" + code = """ +package main + +type User struct { + Name string + Age int +} + +type UserRepository interface { + FindByID(id int) (*User, error) + Save(user *User) error +} + +type Handler func(w http.ResponseWriter, r *http.Request) + +func NewUser(name string, age int) *User { + return &User{Name: name, Age: age} +} + +func (u *User) Greet() string { + return fmt.Sprintf("Hello, %s", u.Name) +} + +func (u *User) SetAge(age int) { + u.Age = age +} +""" + (temp_dir / "test.go").write_text(code) + result = outliner.invoke("test.go") + + assert isinstance(result, list) + signatures = [item["signature"] for item in result] + + assert any("type User struct" in s for s in signatures) + assert any("type UserRepository interface" in s for s in signatures) + assert any("type Handler func" in s for s in signatures) + assert any("func NewUser" in s for s in signatures) + assert any("(u *User) Greet" in s for s in signatures) + assert any("(u *User) SetAge" in s for s in signatures) + + def test_java_outline(self, outliner, temp_dir): + """Test Java file outline extraction.""" + code = """ +package com.example; + +import java.util.List; + +public class UserService { + private String name; + + public UserService(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + private void processInternal() { + // internal logic + } + + public static UserService createDefault() { + return new UserService("default"); + } +} + +interface Repository { + T findById(long id); + void save(T entity); +} + +enum Status { + ACTIVE, + INACTIVE +} + +abstract class BaseService { + public abstract void initialize(); +} +""" + (temp_dir / "Test.java").write_text(code) + result = outliner.invoke("Test.java") + + assert isinstance(result, list) + signatures = [item["signature"] for item in result] + + assert any("class UserService" in s for s in signatures) + assert any("interface Repository" in s for s in signatures) + assert any("enum Status" in s for s in signatures) + assert any("abstract class BaseService" in s for s in signatures) + # Methods + assert any("UserService(" in s for s in signatures) + assert any("getName(" in s for s in signatures) + assert any("processInternal(" in s for s in signatures) + assert any("createDefault(" in s for s in signatures) + + def test_c_outline(self, outliner, temp_dir): + """Test C file outline extraction.""" + code = """ +#include + +typedef struct { + int x; + int y; +} Point; + +struct User { + char name[50]; + int age; +}; + +enum Status { + STATUS_OK, + STATUS_ERROR +}; + +int add(int a, int b) { + return a + b; +} + +void print_hello(void) { + printf("Hello\\n"); +} + +double calculate(double x) { + return x * 2.0; +} +""" + (temp_dir / "test.c").write_text(code) + result = outliner.invoke("test.c") + + assert isinstance(result, list) + signatures = [item["signature"] for item in result] + + # Note: typedef struct { ... } Point; matches "typedef struct" line, not "Point" + assert any("typedef struct" in s for s in signatures) + assert any("struct User" in s for s in signatures) + assert any("enum Status" in s for s in signatures) + assert any("int add" in s for s in signatures) + assert any("void print_hello" in s for s in signatures) + assert any("double calculate" in s for s in signatures) + + def test_cpp_outline(self, outliner, temp_dir): + """Test C++ file outline extraction.""" + code = """ +#include +#include + +namespace myapp { + +class User { +private: + std::string name; +public: + User(const std::string& name); + std::string getName() const; +}; + +struct Point { + int x, y; +}; + +template class Container { +public: + void add(const T& item); + T get(int index) const; +}; + +} // namespace myapp + +void helper_function() { + // helper +} +""" + (temp_dir / "test.cpp").write_text(code) + result = outliner.invoke("test.cpp") + + assert isinstance(result, list) + signatures = [item["signature"] for item in result] + + assert any("class User" in s for s in signatures) + assert any("struct Point" in s for s in signatures) + assert any("namespace myapp" in s for s in signatures) + assert any("template" in s and "Container" in s for s in signatures) + assert any("helper_function" in s for s in signatures) + + def test_php_outline(self, outliner, temp_dir): + """Test PHP file outline extraction.""" + code = """repository = $repository; + } + + public function getUser(int $id): ?User { + return $this->repository->find($id); + } + + private function validateData(array $data): bool { + return true; + } + + public static function createDefault(): self { + return new self(null); + } +} + +interface UserRepositoryInterface { + public function find(int $id): ?User; + public function save(User $user): bool; +} + +trait LoggingTrait { + protected function log(string $message): void { + echo $message; + } +} +""" + (temp_dir / "test.php").write_text(code) + result = outliner.invoke("test.php") + + assert isinstance(result, list) + signatures = [item["signature"] for item in result] + + assert any("class AbstractService" in s for s in signatures) + assert any("final class UserService" in s for s in signatures) + assert any("interface UserRepositoryInterface" in s for s in signatures) + assert any("trait LoggingTrait" in s for s in signatures) + # Methods + assert any("function __construct" in s for s in signatures) + assert any("function getUser" in s for s in signatures) + assert any("function validateData" in s for s in signatures) + assert any("static function createDefault" in s for s in signatures) + + def test_unsupported_file_type(self, outliner, temp_dir): + """Test that unsupported file types return an error.""" + (temp_dir / "test.xyz").write_text("some content") + result = outliner.invoke("test.xyz") + + assert isinstance(result, str) + assert "Error" in result + assert "Unsupported file type" in result + + def test_file_not_found(self, outliner): + """Test that non-existent files return an error.""" + result = outliner.invoke("nonexistent.py") + + assert isinstance(result, str) + assert "Error" in result + assert "not found" in result + + def test_empty_file(self, outliner, temp_dir): + """Test that empty files return empty outline.""" + (temp_dir / "empty.py").write_text("") + result = outliner.invoke("empty.py") + + assert isinstance(result, list) + assert len(result) == 0 + + def test_output_has_line_numbers(self, outliner, temp_dir): + """Test that output includes line numbers.""" + code = """def foo(): + pass + +class Bar: + pass +""" + (temp_dir / "test.py").write_text(code) + result = outliner.invoke("test.py") + + assert isinstance(result, list) + for item in result: + assert "line" in item + assert isinstance(item["line"], int) + assert "signature" in item + assert isinstance(item["signature"], str) + + +class TestFileFragmentReaderTool: + """Tests for FileFragmentReaderTool.""" + + @pytest.fixture + def temp_dir(self): + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + @pytest.fixture + def reader(self, temp_dir): + return FileFragmentReaderTool(str(temp_dir)) + + def test_read_fragment(self, reader, temp_dir): + """Test reading a fragment of a file.""" + content = "line1\nline2\nline3\nline4\nline5" + (temp_dir / "test.txt").write_text(content) + + result = reader.invoke("test.txt:2:4") + assert result == "line2\nline3\nline4\n" + + def test_invalid_format(self, reader): + """Test that invalid format returns error.""" + result = reader.invoke("invalid") + assert "Error" in result + assert "Invalid input format" in result + + +class TestStructureExplorerTool: + """Tests for StructureExplorerTool.""" + + @pytest.fixture + def temp_dir(self): + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + @pytest.fixture + def explorer(self, temp_dir): + return StructureExplorerTool(str(temp_dir)) + + def test_discover_structure(self, explorer, temp_dir): + """Test directory structure discovery.""" + (temp_dir / "file1.py").write_text("# file1") + (temp_dir / "file2.py").write_text("# file2") + (temp_dir / "subdir").mkdir() + (temp_dir / "subdir" / "file3.py").write_text("# file3") + + result = explorer.invoke("2") + + assert result["type"] == "directory" + children_names = [c["name"] for c in result["children"]] + assert "file1.py" in children_names + assert "file2.py" in children_names + assert "subdir" in children_names + + +class TestLanguagePatterns: + """Tests to verify all language patterns are valid regex.""" + + def test_all_patterns_compile(self): + """Verify all patterns in LANGUAGE_PATTERNS compile successfully.""" + import re + + for language, patterns in LANGUAGE_PATTERNS.items(): + for pattern in patterns: + # Should not raise exception + compiled = re.compile(pattern) + assert compiled is not None From 87ac6dc01bda37bf12b18f0ac52dd883d6cc8da5 Mon Sep 17 00:00:00 2001 From: Jean Silva Date: Tue, 17 Feb 2026 23:15:55 +0100 Subject: [PATCH 3/5] feat(core): centralize default model via DEFAULT_MODEL constant Introduce a DEFAULT_MODEL constant and use it throughout core agents to avoid hardcoded model names. Add constants.py support for picking the default model from the environment (OPENAI_MODEL_NAME) with a fallback to "gpt-4o-mini", and expand the .env.example to document the OPENAI_MODEL_NAME and common OpenAI-compatible base URLs. - Add DEFAULT_MODEL in agentic_framework/constants.py and read from OPENAI_MODEL_NAME env var. - Replace hardcoded "gpt-5-nano" with DEFAULT_MODEL in: - langgraph_agent.py - travel_coordinator_agent.py - simple_agent.py - Update .env.example to document OPENAI_MODEL_NAME and optional OPENAI_BASE_URL entries for various providers. This centralizes model configuration, makes the default model configurable via environment, and improves maintainability and clarity. --- .env.example | 10 ++++++++++ agentic-framework/src/agentic_framework/constants.py | 3 +++ .../src/agentic_framework/core/langgraph_agent.py | 3 ++- .../src/agentic_framework/core/simple_agent.py | 3 ++- .../agentic_framework/core/travel_coordinator_agent.py | 3 ++- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 70f0d7f..3641a38 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,16 @@ # Required Environment Variables OPENAI_API_KEY="sk-*****" +# Optional: Model selection (defaults to gpt-4o-mini if not set) +# OPENAI_MODEL_NAME="gpt-4o-mini" +# OPENAI_MODEL_NAME="glm-4.5-air" # z.ai + +# Optional: Use OpenAI-compatible APIs (uncomment one) +# OPENAI_BASE_URL="https://api.z.ai/api/coding/paas/v4" # z.ai +# OPENAI_BASE_URL="https://openrouter.ai/api/v1" # OpenRouter +# OPENAI_BASE_URL="http://localhost:11434/v1" # Ollama (local) +# OPENAI_BASE_URL="http://localhost:1234/v1" # LM Studio (local) + # Agentic Web Search TAVILY_API_KEY="tvly-****" diff --git a/agentic-framework/src/agentic_framework/constants.py b/agentic-framework/src/agentic_framework/constants.py index 55eb104..a30d4ba 100644 --- a/agentic-framework/src/agentic_framework/constants.py +++ b/agentic-framework/src/agentic_framework/constants.py @@ -1,4 +1,7 @@ +import os from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent.parent LOGS_DIR = BASE_DIR / "logs" + +DEFAULT_MODEL = os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini") diff --git a/agentic-framework/src/agentic_framework/core/langgraph_agent.py b/agentic-framework/src/agentic_framework/core/langgraph_agent.py index d658138..d3e07d6 100644 --- a/agentic-framework/src/agentic_framework/core/langgraph_agent.py +++ b/agentic-framework/src/agentic_framework/core/langgraph_agent.py @@ -6,6 +6,7 @@ from langchain_openai import ChatOpenAI from langgraph.checkpoint.memory import InMemorySaver +from agentic_framework.constants import DEFAULT_MODEL from agentic_framework.interfaces.base import Agent from agentic_framework.mcp import MCPProvider @@ -15,7 +16,7 @@ class LangGraphMCPAgent(Agent): def __init__( self, - model_name: str = "gpt-5-nano", + model_name: str = DEFAULT_MODEL, temperature: float = 0.1, mcp_provider: MCPProvider | None = None, initial_mcp_tools: List[Any] | None = None, diff --git a/agentic-framework/src/agentic_framework/core/simple_agent.py b/agentic-framework/src/agentic_framework/core/simple_agent.py index 3c2d59f..6717b43 100644 --- a/agentic-framework/src/agentic_framework/core/simple_agent.py +++ b/agentic-framework/src/agentic_framework/core/simple_agent.py @@ -4,6 +4,7 @@ from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI +from agentic_framework.constants import DEFAULT_MODEL from agentic_framework.interfaces.base import Agent from agentic_framework.registry import AgentRegistry @@ -15,7 +16,7 @@ class SimpleAgent(Agent): No MCP access (mcp_servers=None in registry). """ - def __init__(self, model_name: str = "gpt-5-nano", temperature: float = 0.0, **kwargs: Any) -> None: + def __init__(self, model_name: str = DEFAULT_MODEL, temperature: float = 0.0, **kwargs: Any) -> None: self.model = ChatOpenAI(model=model_name, temperature=temperature) self.prompt = ChatPromptTemplate.from_messages( [("system", "You are a helpful assistant."), ("user", "{input}")] diff --git a/agentic-framework/src/agentic_framework/core/travel_coordinator_agent.py b/agentic-framework/src/agentic_framework/core/travel_coordinator_agent.py index 356a094..96dab96 100644 --- a/agentic-framework/src/agentic_framework/core/travel_coordinator_agent.py +++ b/agentic-framework/src/agentic_framework/core/travel_coordinator_agent.py @@ -3,6 +3,7 @@ from langchain_core.messages import BaseMessage +from agentic_framework.constants import DEFAULT_MODEL from agentic_framework.core.langgraph_agent import LangGraphMCPAgent from agentic_framework.interfaces.base import Agent from agentic_framework.mcp import MCPProvider @@ -54,7 +55,7 @@ class TravelCoordinatorAgent(Agent): def __init__( self, - model_name: str = "gpt-5-nano", + model_name: str = DEFAULT_MODEL, temperature: float = 0.2, mcp_provider: MCPProvider | None = None, initial_mcp_tools: List[Any] | None = None, From 71b0c85ef073ea6e36176c428e25bd1565110f9b Mon Sep 17 00:00:00 2001 From: Jean Silva Date: Wed, 18 Feb 2026 17:17:34 +0100 Subject: [PATCH 4/5] chore(cli): load .env before importing constants Call load_dotenv() at the top of cli.py so environment variables are loaded before any module-level imports that read them (notably agentic_framework.constants). This prevents constants from being initialized with missing environment values and avoids subtle runtime errors. Also remove the duplicate load_dotenv() call and adjust import ordering for clarity. --- agentic-framework/src/agentic_framework/cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/agentic-framework/src/agentic_framework/cli.py b/agentic-framework/src/agentic_framework/cli.py index 6d8832d..2a35954 100644 --- a/agentic-framework/src/agentic_framework/cli.py +++ b/agentic-framework/src/agentic_framework/cli.py @@ -1,18 +1,19 @@ +from dotenv import load_dotenv + +load_dotenv() # Must be called before importing constants (which reads env vars) + import asyncio import logging import traceback from typing import Any, Callable, Type import typer -from dotenv import load_dotenv from rich.console import Console from agentic_framework.constants import LOGS_DIR from agentic_framework.mcp import MCPConnectionError, MCPProvider from agentic_framework.registry import AgentRegistry -load_dotenv() - RUN_TIMEOUT_SECONDS = 600 app = typer.Typer( From 445edbe5ddce8946e1fb7d6895c7a0474145c15d Mon Sep 17 00:00:00 2001 From: Jean Silva Date: Wed, 18 Feb 2026 17:42:00 +0100 Subject: [PATCH 5/5] refactor(env): centralize dotenv loading and tidy Makefile Move dotenv loading out of the CLI module and into the constants module so environment variables are loaded once at startup before any module reads them. This prevents subtle ordering issues where modules that import constants may read env vars before they are loaded. Update Makefile targets: - Rename and reorganize check/format/fix/lint targets: - Add check target that runs mypy, ruff check and ruff format --check (no modifications). - Add fix target that runs ruff check --fix and ruff format to apply automatic fixes. - Remove separate format and lint targets and simplify default target list to include fix and check. - Adjust .PHONY to reflect updated target names. These changes ensure environment handling is centralized and Makefile targets are clearer about which commands modify files versus which only verify state. --- Makefile | 16 ++++++---------- agentic-framework/src/agentic_framework/cli.py | 4 ---- .../src/agentic_framework/constants.py | 4 ++++ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 1e52e5b..598db4b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install run test clean lint check format docker-build docker-clean +.PHONY: help install run test clean fix check docker-build docker-clean .DEFAULT_GOAL := help # Use `uv` for python environment management @@ -31,19 +31,15 @@ run: ## Run the agentic-run simple command to exemplify the CLI test: ## Run tests with coverage @$(UV) --project $(PROJECT_DIR) run pytest $(PROJECT_DIR)/tests/ -v --cov=$(PROJECT_DIR)/src --cov-report=xml --cov-report=term -lint: ## Run ruff linting with auto-fix - @$(UV) --project $(PROJECT_DIR) run mypy $(PROJECT_DIR)/src/ - @$(UV) --project $(PROJECT_DIR) run ruff check --fix $(PROJECT_DIR)/src/ $(PROJECT_DIR)/tests/ - @$(UV) --project $(PROJECT_DIR) run ruff format $(PROJECT_DIR)/src/ $(PROJECT_DIR)/tests/ - -format: ## Run ruff code formatting - @$(UV) --project $(PROJECT_DIR) run ruff format $(PROJECT_DIR)/src/ $(PROJECT_DIR)/tests/ - -check: ## Run lint, format check, and mypy type checking (no fixes) +check: ## Run all checks (mypy, ruff lint, ruff format) - no modifications @$(UV) --project $(PROJECT_DIR) run mypy $(PROJECT_DIR)/src/ @$(UV) --project $(PROJECT_DIR) run ruff check $(PROJECT_DIR)/src/ $(PROJECT_DIR)/tests/ @$(UV) --project $(PROJECT_DIR) run ruff format --check $(PROJECT_DIR)/src/ $(PROJECT_DIR)/tests/ +fix: ## Auto-fix lint and format issues (runs ruff check --fix and ruff format) + @$(UV) --project $(PROJECT_DIR) run ruff check --fix $(PROJECT_DIR)/src/ $(PROJECT_DIR)/tests/ + @$(UV) --project $(PROJECT_DIR) run ruff format $(PROJECT_DIR)/src/ $(PROJECT_DIR)/tests/ + clean: ## Deep clean temporary files and virtual environment rm -rf $(PROJECT_DIR)/.venv rm -rf $(PROJECT_DIR)/../.pytest_cache/ diff --git a/agentic-framework/src/agentic_framework/cli.py b/agentic-framework/src/agentic_framework/cli.py index 2a35954..46879e0 100644 --- a/agentic-framework/src/agentic_framework/cli.py +++ b/agentic-framework/src/agentic_framework/cli.py @@ -1,7 +1,3 @@ -from dotenv import load_dotenv - -load_dotenv() # Must be called before importing constants (which reads env vars) - import asyncio import logging import traceback diff --git a/agentic-framework/src/agentic_framework/constants.py b/agentic-framework/src/agentic_framework/constants.py index a30d4ba..5a5461e 100644 --- a/agentic-framework/src/agentic_framework/constants.py +++ b/agentic-framework/src/agentic_framework/constants.py @@ -1,6 +1,10 @@ import os from pathlib import Path +from dotenv import load_dotenv + +load_dotenv() # Load .env before reading environment variables + BASE_DIR = Path(__file__).resolve().parent.parent.parent LOGS_DIR = BASE_DIR / "logs"