Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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-****"

Expand Down
13 changes: 12 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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+.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
16 changes: 6 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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/
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
3 changes: 0 additions & 3 deletions agentic-framework/src/agentic_framework/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
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(
Expand Down
7 changes: 7 additions & 0 deletions agentic-framework/src/agentic_framework/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
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"

DEFAULT_MODEL = os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini")
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion agentic-framework/src/agentic_framework/core/simple_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
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}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion agentic-framework/src/agentic_framework/mcp/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
133 changes: 123 additions & 10 deletions agentic-framework/src/agentic_framework/tools/codebase_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -77,33 +168,55 @@ 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:
return "get_file_outline"

@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):
Expand Down
7 changes: 4 additions & 3 deletions agentic-framework/src/agentic_framework/tools/web_search.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict
from typing import Any, Dict, cast

from tavily import TavilyClient # type: ignore[import-untyped]

Expand All @@ -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
Expand All @@ -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)
Loading