diff --git a/.env.example b/.env.example
index 3641a38..face1b2 100644
--- a/.env.example
+++ b/.env.example
@@ -11,9 +11,6 @@ OPENAI_API_KEY="sk-*****"
# 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-****"
-
# Optional Environment Variables
# Evaluation and tracing with Langsmith
diff --git a/README.md b/README.md
index d0f7886..377c795 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,7 @@ Instead of spending days wiring together LLMs, tools, and execution environments
|-------|---------|-------------|-------------|
| `developer` | **Code Master:** Read, search & edit code. | `webfetch` | *All codebase tools below* |
| `travel-coordinator` | **Trip Planner:** Orchestrates agents. | `kiwi-com-flight-search`
`webfetch` | *Uses 3 sub-agents* |
-| `chef` | **Chef:** Recipes from your fridge. | *Tavily Web Search* | `web_search` |
+| `chef` | **Chef:** Recipes from your fridge. | `webfetch` | - |
| `news` | **News Anchor:** Aggregates top stories. | `webfetch` | - |
| `travel` | **Flight Booker:** Finds the best routes. | `kiwi-com-flight-search` | - |
| `simple` | **Chat Buddy:** Vanilla conversational agent. | - | - |
@@ -80,7 +80,6 @@ Instead of spending days wiring together LLMs, tools, and execution environments
| `read_file_fragment` | Precise file reading | `file.py:10:50` |
| `code_search` | Fast search via `ripgrep` | Global regex search |
| `edit_file` | Safe file editing | Inserts/Replaces lines |
-| `web_search` | Web search | `query` |
📝 Advanced: edit_file Formats
@@ -100,9 +99,7 @@ Instead of spending days wiring together LLMs, tools, and execution environments
| Server | Purpose | API Key Needed? |
|--------|---------|-----------------|
| `kiwi-com-flight-search` | Search real-time flights | 🟢 No |
-| `webfetch` | Extract clean text from URLs | 🟢 No |
-| `tavily` | Fast Web search | 🔴 Yes (`TAVILY_API_KEY`) |
-| `tinyfish` | AI assistant | 🔴 Yes (`TINYFISH_API_KEY`) |
+| `webfetch` | Extract clean text from URLs & web search | 🟢 No |
---
@@ -146,8 +143,6 @@ bin/agent.sh chef -i "I have chicken, rice, and soy sauce. What can I make?"
|----------|-----------|-------------|
| `OPENAI_API_KEY` | 🟢 **Yes*** | OpenAI API key (*if using OpenAI) |
| `ANTHROPIC_API_KEY`| 🟢 **Yes*** | Anthropic API key (*if using Anthropic) |
-| `TAVILY_API_KEY` | 🟡 **For chef** | Tavily search API key |
-| `TINYFISH_API_KEY` | ⚪ No | TinyFish MCP access |
| `OPENAI_MODEL_NAME` | ⚪ No | Model to use (default: `gpt-4o`/`gpt-4`) |
diff --git a/agentic-framework/PLAN_FILE_EDITING_IMPROVEMENTS.md b/agentic-framework/PLAN_FILE_EDITING_IMPROVEMENTS.md
new file mode 100644
index 0000000..7e9f6c5
--- /dev/null
+++ b/agentic-framework/PLAN_FILE_EDITING_IMPROVEMENTS.md
@@ -0,0 +1,401 @@
+# Plan: Production-Grade File Editing for AI Agents
+
+## Problem Statement
+
+The DeveloperAgent makes incorrect edits that corrupt files:
+1. Edits at wrong line numbers (doesn't read first)
+2. Removes syntax elements like quotes (doesn't copy exactly)
+3. Ignores SYNTAX WARNING output (no self-correction)
+
+## Root Cause Analysis
+
+1. **Agent doesn't read before editing** - It guesses line numbers
+2. **Tool output doesn't guide correction** - Warnings are shown but not actionable
+3. **No enforcement of workflow** - Agent can skip the read step
+4. **Line-number-only editing is fragile** - Small changes break line calculations
+
+## Implementation Plan
+
+### Phase 1: Update System Prompt (P0 - Critical, Low Effort)
+
+**File**: `src/agentic_framework/core/developer_agent.py`
+
+Add explicit read-first workflow instructions to the system prompt:
+
+```python
+@property
+def system_prompt(self) -> str:
+ return """You are a software engineer assistant with codebase exploration and editing capabilities.
+
+## AVAILABLE TOOLS
+
+1. **find_files** - Fast file search by name using fd
+2. **discover_structure** - Directory tree exploration
+3. **get_file_outline** - Extract class/function signatures (multi-language)
+4. **read_file_fragment** - Read specific line ranges
+5. **code_search** - Fast pattern search via ripgrep
+6. **edit_file** - Edit files with line-based operations
+7. **webfetch** (MCP) - Fetch web content
+
+## MANDATORY FILE EDITING WORKFLOW
+
+Before using edit_file, you MUST follow this sequence:
+
+### Step 1: READ FIRST (Required)
+- Use `read_file_fragment` to see the EXACT lines you plan to modify
+- Never guess line numbers - always verify them
+- Example: `read_file_fragment("example.py:88:95")`
+
+### Step 2: COPY EXACTLY
+When providing replacement content:
+- Copy the exact text including all quotes, indentation, and punctuation
+- Do NOT truncate, paraphrase, or summarize
+- Preserve docstring delimiters (`"""` or `'''`)
+- Maintain exact indentation (tabs vs spaces)
+
+### Step 3: APPLY EDIT
+Use edit_file with precise parameters:
+- Colon format: `replace:path:start:end:content`
+- JSON format for complex content: `{"op": "replace", "path": "...", "start": N, "end": N, "content": "..."}`
+
+### Step 4: VERIFY
+After editing:
+- Check the result message for SYNTAX WARNING
+- If warning appears, read the affected lines and fix immediately
+- Do not exceed 3 retry attempts
+
+### Error Recovery
+If edit_file returns an error:
+1. READ the file first using read_file_fragment
+2. Understand what went wrong from the error message
+3. Apply a corrected edit
+4. Maximum 3 retries per edit
+
+## EDIT_FILE OPERATIONS
+
+- `replace:path:start:end:content` - Replace lines start to end (1-indexed)
+- `insert:path:after_line:content` - Insert after line number
+- `insert:path:before_line:content` - Insert before line number
+- `delete:path:start:end` - Delete lines start to end
+
+## IMPORTANT REMINDERS
+
+- Line numbers are 1-indexed
+- Content uses `\\n` for newlines in colon format
+- Always read before editing - NO EXCEPTIONS
+- You also have access to MCP tools like `webfetch` if you need to fetch information from the web
+
+Always provide clear, concise explanations and suggest improvements when relevant.
+"""
+```
+
+---
+
+### Phase 2: Add SEARCH/REPLACE Operation (P1 - High Impact, Medium Effort)
+
+**File**: `src/agentic_framework/tools/codebase_explorer.py`
+
+Add a new operation type that finds and replaces exact text (more robust than line numbers):
+
+```python
+def _handle_json_input(self, input_str: str) -> Any:
+ """Handle JSON-formatted input for complex content."""
+ import json
+
+ data = json.loads(input_str)
+ op = data.get("op")
+ path = data.get("path")
+ content = data.get("content", "")
+
+ if op == "replace":
+ return self._replace_lines(path, data["start"], data["end"], content)
+ elif op == "search_replace":
+ return self._search_replace(path, data["old"], data.get("new", ""))
+ elif op == "insert":
+ return self._insert_lines(path, data.get("after"), data.get("before"), content)
+ elif op == "delete":
+ return self._delete_lines(path, data["start"], data["end"])
+ else:
+ return f"Error: Unknown operation '{op}'"
+
+def _search_replace(self, path: str, old_text: str, new_text: str) -> str:
+ """Find and replace exact text in file.
+
+ More robust than line-based editing because it doesn't require line numbers.
+ Fails if old_text is not found or found multiple times.
+ """
+ full_path = self._validate_path(path)
+
+ if not full_path.exists():
+ return f"Error: File '{path}' not found"
+
+ warning = self._check_file_size(full_path)
+
+ try:
+ with open(full_path, "r", encoding="utf-8") as f:
+ content = f.read()
+ except UnicodeDecodeError:
+ return f"Error: File '{path}' is not valid UTF-8 text"
+
+ # Count matches
+ count = content.count(old_text)
+
+ if count == 0:
+ # Try fuzzy match (strip trailing whitespace differences)
+ old_normalized = old_text.rstrip()
+ matches = [i for i in range(len(content)) if content[i:i+len(old_normalized)].rstrip() == old_normalized]
+ if len(matches) == 1:
+ # Found with fuzzy match, use it
+ count = 1
+ else:
+ return self._format_search_error(content, old_text)
+
+ if count > 1:
+ return f"Error: Found {count} occurrences of the search text in '{path}'. " \
+ f"Make the search text more specific (include more context lines)."
+
+ # Perform replacement
+ new_content = content.replace(old_text, new_text, 1)
+
+ self._atomic_write(full_path, new_content)
+
+ # Find line numbers for reporting
+ lines_before = content[:content.index(old_text)].count('\n') + 1
+ lines_after = old_text.count('\n')
+ end_line = lines_before + lines_after
+
+ result = f"Replaced text at lines {lines_before}-{end_line} in '{path}'"
+ if warning:
+ result = f"{warning}\n{result}"
+
+ # Validate syntax after edit
+ syntax_warning = self._validate_syntax(new_content, path)
+ if syntax_warning:
+ result = f"{result}{syntax_warning}"
+
+ return result
+
+def _format_search_error(self, content: str, old_text: str) -> str:
+ """Format helpful error message when search text not found."""
+ # Try to find similar text
+ old_lines = old_text.strip().split('\n')
+ if old_lines:
+ first_line = old_lines[0].strip()
+ for i, line in enumerate(content.split('\n'), 1):
+ if first_line in line:
+ return (
+ f"Error: Search text not found exactly.\n"
+ f"Found similar text at line {i}: {line.strip()[:50]}...\n"
+ f"Tip: Use read_file_fragment to see the exact content."
+ )
+
+ return (
+ f"Error: Search text not found in file.\n"
+ f"Tip: Use read_file_fragment to view the file content first, "
+ f"then copy the exact text to replace."
+ )
+```
+
+Update the description property:
+
+```python
+@property
+def description(self) -> str:
+ return """Edit files with line-based or text-based operations.
+
+Operations:
+- replace:path:start:end:content - Replace lines start to end
+- insert:path:after_line:content - Insert after line number
+- insert:path:before_line:content - Insert before line number
+- delete:path:start:end - Delete lines start to end
+
+For complex content with colons/newlines, use JSON:
+{"op": "replace", "path": "...", "start": N, "end": N, "content": "..."}
+
+RECOMMENDED: Use search_replace for precise edits (no line numbers needed):
+{"op": "search_replace", "path": "...", "old": "exact text to find", "new": "replacement text"}
+
+Line numbers are 1-indexed. Content uses \\n for newlines.
+ALWAYS read the file first using read_file_fragment to verify content before editing.
+"""
+```
+
+---
+
+### Phase 3: Enhance Error Messages (P2 - Medium Effort, Medium Impact)
+
+**File**: `src/agentic_framework/tools/codebase_explorer.py`
+
+Add structured error types and suggested actions:
+
+```python
+from dataclasses import dataclass
+from typing import Optional
+
+@dataclass
+class EditError:
+ """Structured error information for edit operations."""
+ error_type: str
+ message: str
+ suggested_action: str
+ context: dict
+
+ def __str__(self) -> str:
+ return f"""Error ({self.error_type}): {self.message}
+
+Suggested action: {self.suggested_action}
+Context: {self.context}
+"""
+
+# Update validation methods to use structured errors
+def _validate_line_bounds(self, lines: List[str], start: int, end: int) -> None:
+ """Validate line numbers are within bounds. Raises ValueError with helpful message."""
+ total_lines = len(lines)
+
+ if start < 1:
+ raise ValueError(f"Start line must be >= 1, got {start}. Line numbers are 1-indexed.")
+ if end < start:
+ raise ValueError(f"End line ({end}) must be >= start line ({start}).")
+ if start > total_lines:
+ raise ValueError(
+ f"Start line ({start}) exceeds file length ({total_lines}). "
+ f"Use read_file_fragment to verify line numbers before editing."
+ )
+ if end > total_lines + 1:
+ raise ValueError(
+ f"End line ({end}) exceeds file length + 1 ({total_lines + 1}). "
+ f"Use read_file_fragment to verify line numbers before editing."
+ )
+```
+
+---
+
+### Phase 4: Add Verify Edit Tool (P3 - Optional)
+
+**File**: `src/agentic_framework/tools/codebase_explorer.py`
+
+```python
+class VerifyEditTool(CodebaseExplorer, Tool):
+ """Tool to verify recent edits by reading back and checking syntax."""
+
+ @property
+ def name(self) -> str:
+ return "verify_edit"
+
+ @property
+ def description(self) -> str:
+ return """Verify an edit by reading affected lines and checking syntax.
+ Input: 'path:start:end' of the edited region.
+ Returns: Current content and syntax validation result.
+ Use this after every edit to confirm changes were applied correctly.
+ """
+
+ def invoke(self, input_str: str) -> str:
+ try:
+ parts = input_str.split(":")
+ if len(parts) < 3:
+ return "Error: Invalid input format. Use 'path:start:end'."
+ file_path = ":".join(parts[:-2])
+ start_line = int(parts[-2])
+ end_line = int(parts[-1])
+
+ full_path = self.root_dir / file_path
+ if not full_path.exists():
+ return f"Error: File {file_path} not found."
+
+ with open(full_path, "r", encoding="utf-8") as f:
+ lines = f.readlines()
+
+ content = "".join(lines[max(0, start_line - 1):end_line])
+
+ # Validate syntax
+ from agentic_framework.tools.syntax_validator import get_validator
+ result = get_validator().validate("".join(lines), file_path)
+
+ output = [f"Lines {start_line}-{end_line} in '{file_path}':", "", content]
+
+ if result.skipped:
+ output.append(f"\n(Syntax check skipped: {result.skip_reason})")
+ elif not result.is_valid:
+ output.append(f"\nSYNTAX WARNING:{result.warning_message}")
+ else:
+ output.append("\n✓ Syntax validation passed")
+
+ return "\n".join(output)
+
+ except Exception as e:
+ return f"Error: {e}"
+```
+
+---
+
+## Testing Plan
+
+### Unit Tests
+
+1. Test search_replace with exact match
+2. Test search_replace with no match (error message)
+3. Test search_replace with multiple matches (error message)
+4. Test fuzzy matching for whitespace differences
+5. Test system prompt contains read-first instructions
+6. Test verify_edit tool
+
+### Integration Tests
+
+1. Test agent workflow: read → edit → verify
+2. Test error recovery: agent sees error, reads file, retries
+3. Test syntax warning triggers verification
+
+### Manual Testing
+
+```bash
+# Test 1: Simple text replacement
+uv --directory agentic-framework run agentic-run developer -i "Use search_replace to change 'A mock weather tool.' to 'A mock weather tool for testing.' in example.py"
+
+# Test 2: Complex edit with verification
+uv --directory agentic-framework run agentic-run developer -i "Update the CalculatorTool docstring to mention it also supports abs, round, min, max, sum functions"
+
+# Test 3: Error recovery
+uv --directory agentic-framework run agentic-run developer -i "Replace a non-existent string in example.py"
+```
+
+---
+
+## Validation Checklist
+
+- [x] System prompt updated with read-first workflow
+- [x] search_replace operation implemented
+- [x] Error messages include suggested actions
+- [x] Unit tests pass (133 tests)
+- [x] make check passes
+- [x] make test passes
+- [ ] Manual testing successful (run developer agent test)
+
+---
+
+## Rollback Plan
+
+If issues arise:
+1. Revert system prompt changes (single commit)
+2. Disable search_replace by removing from _handle_json_input
+3. Fall back to line-based editing only
+
+---
+
+## Dependencies
+
+- tree-sitter==0.21.3 (already pinned)
+- tree-sitter-languages>=1.10.2 (already installed)
+
+---
+
+## Estimated Effort
+
+| Phase | Effort | Risk |
+|-------|--------|------|
+| Phase 1: System Prompt | 30 min | Low |
+| Phase 2: SEARCH/REPLACE | 2 hours | Medium |
+| Phase 3: Error Messages | 1 hour | Low |
+| Phase 4: Verify Tool | 1 hour | Low |
+| Testing | 2 hours | Low |
+| **Total** | **6.5 hours** | Medium |
diff --git a/agentic-framework/TODO.md b/agentic-framework/TODO.md
index b395c10..82fca23 100644
--- a/agentic-framework/TODO.md
+++ b/agentic-framework/TODO.md
@@ -126,13 +126,6 @@ brew install fd ripgrep fzf
apt install fd-find ripgrep fzf
```
-### API Keys Required
-
-- `TAVILY_API_KEY` - For Tavily MCP server
-- `TINYFISH_API_KEY` - For TinyFish MCP server (optional)
-
-Note: Missing keys produce warnings but don't prevent agent startup.
-
## Deprecated / Legacy
None currently.
diff --git a/agentic-framework/pyproject.toml b/agentic-framework/pyproject.toml
index 1dd415c..837306f 100644
--- a/agentic-framework/pyproject.toml
+++ b/agentic-framework/pyproject.toml
@@ -11,9 +11,6 @@ dependencies = [
"langchain-openai>=1.1.1",
"langchain-anthropic>=1.0.3",
"langchain-model-profiles>=0.0.4",
- "langchain-tavily>=0.2.13",
- "tavily-python>=0.7.13",
- "tavily>=1.1.0",
"langchain-community>=0.4.1",
"langchain-text-splitters>=1.0.0",
"mcp>=1.21.1",
diff --git a/agentic-framework/src/agentic_framework/core/chef_agent.py b/agentic-framework/src/agentic_framework/core/chef_agent.py
index 4359c7c..0192373 100644
--- a/agentic-framework/src/agentic_framework/core/chef_agent.py
+++ b/agentic-framework/src/agentic_framework/core/chef_agent.py
@@ -1,30 +1,16 @@
-from typing import Any, Sequence
-
-from langchain_core.tools import StructuredTool
-
from agentic_framework.core.langgraph_agent import LangGraphMCPAgent
from agentic_framework.registry import AgentRegistry
-from agentic_framework.tools.web_search import WebSearchTool
-@AgentRegistry.register("chef", mcp_servers=["tavily"])
+@AgentRegistry.register("chef", mcp_servers=["web-fetch"])
class ChefAgent(LangGraphMCPAgent):
- """A recipe-focused agent with local web search plus optional MCP tools."""
+ """A recipe-focused agent with MCP web search capabilities."""
@property
def system_prompt(self) -> str:
return """You are a personal chef.
The user will give you a list of ingredients they have left over in their house.
- Using the web search tool, search the web for recipes
+ Using the web search tool (web_fetch_search), search the web for recipes
that can be made with the ingredients they have.
Return recipe suggestions and eventually the recipe instructions
to the user, if requested."""
-
- def local_tools(self) -> Sequence[Any]:
- return [
- StructuredTool.from_function(
- func=WebSearchTool().invoke,
- name="web_search",
- description="Search the web for information given a query.",
- )
- ]
diff --git a/agentic-framework/src/agentic_framework/mcp/config.py b/agentic-framework/src/agentic_framework/mcp/config.py
index acb6bbc..1b4d518 100644
--- a/agentic-framework/src/agentic_framework/mcp/config.py
+++ b/agentic-framework/src/agentic_framework/mcp/config.py
@@ -4,7 +4,6 @@
is defined in AgentRegistry.register(..., mcp_servers=...).
"""
-import os
from typing import Any, Dict
# All available MCP servers. Each entry must include "transport".
@@ -14,18 +13,10 @@
"url": "https://mcp.kiwi.com",
"transport": "sse",
},
- "tinyfish": {
- "url": "https://agent.tinyfish.ai/mcp",
- "transport": "sse",
- },
"web-fetch": {
"url": "https://remote.mcpservers.org/fetch/mcp",
"transport": "http",
},
- "tavily": {
- "url": "https://mcp.tavily.com/mcp",
- "transport": "sse",
- },
}
@@ -34,8 +25,7 @@ def get_mcp_servers_config(
) -> Dict[str, Dict[str, Any]]:
"""Return MCP server config for MultiServerMCPClient.
- Merges DEFAULT_MCP_SERVERS with optional override, then resolves
- env-dependent values (e.g. TAVILY_API_KEY). Does not mutate any shared state.
+ Merges DEFAULT_MCP_SERVERS with optional override. Does not mutate any shared state.
"""
base = {k: dict(v) for k, v in DEFAULT_MCP_SERVERS.items()}
if override:
@@ -47,22 +37,4 @@ def get_mcp_servers_config(
def _resolve_server_config(server_name: str, raw: Dict[str, Any]) -> Dict[str, Any]:
"""Return a copy of server config with env-dependent values resolved."""
- import logging
-
- out = dict(raw)
- if server_name == "tavily":
- key = os.environ.get("TAVILY_API_KEY")
- if not key:
- logging.warning("TAVILY_API_KEY not found in environment. Tavily MCP may fail to connect.")
- else:
- base = (raw.get("url") or "").rstrip("/")
- sep = "&" if "?" in base else "?"
- out["url"] = f"{base}{sep}tavilyApiKey={key}"
- elif server_name == "tinyfish":
- key = os.environ.get("TINYFISH_API_KEY")
- if not key:
- logging.warning("TINYFISH_API_KEY not found in environment. TinyFish MCP may fail to connect.")
- else:
- out["headers"] = out.get("headers", {})
- out["headers"]["X-API-Key"] = key
- return out
+ return dict(raw)
diff --git a/agentic-framework/src/agentic_framework/tools/__init__.py b/agentic-framework/src/agentic_framework/tools/__init__.py
index 1eaadc0..51aab65 100644
--- a/agentic-framework/src/agentic_framework/tools/__init__.py
+++ b/agentic-framework/src/agentic_framework/tools/__init__.py
@@ -8,12 +8,10 @@
)
from .example import CalculatorTool, WeatherTool
from .syntax_validator import SyntaxValidator, ValidationResult, get_validator
-from .web_search import WebSearchTool
__all__ = [
"CalculatorTool",
"WeatherTool",
- "WebSearchTool",
"CodeSearcher",
"StructureExplorerTool",
"FileOutlinerTool",
diff --git a/agentic-framework/src/agentic_framework/tools/web_search.py b/agentic-framework/src/agentic_framework/tools/web_search.py
deleted file mode 100644
index 5d5aabc..0000000
--- a/agentic-framework/src/agentic_framework/tools/web_search.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from typing import Any, Dict, cast
-
-from tavily import TavilyClient # type: ignore[import-untyped]
-
-from agentic_framework.interfaces.base import Tool
-
-
-class WebSearchTool(Tool):
- """WebSearch Tool that uses Tavily API."""
-
- def __init__(self) -> None:
- self.tavily_client = TavilyClient()
-
- @property
- def name(self) -> str:
- return "web_search"
-
- @property
- def description(self) -> str:
- return "Search the web for information using Tavily API."
-
- def invoke(self, query: str) -> Dict[str, Any]:
- """Search the web for information"""
- result = self.tavily_client.search(query)
- return cast(Dict[str, Any], result)
diff --git a/agentic-framework/tests/test_cli.py b/agentic-framework/tests/test_cli.py
index 56f310e..2460172 100644
--- a/agentic-framework/tests/test_cli.py
+++ b/agentic-framework/tests/test_cli.py
@@ -41,7 +41,7 @@ def test_execute_agent_without_mcp(monkeypatch):
def test_execute_agent_with_mcp(monkeypatch):
monkeypatch.setattr(cli.AgentRegistry, "get", lambda name: FakeAgent)
- monkeypatch.setattr(cli.AgentRegistry, "get_mcp_servers", lambda name: ["tavily"])
+ monkeypatch.setattr(cli.AgentRegistry, "get_mcp_servers", lambda name: ["web-fetch"])
monkeypatch.setattr(cli, "MCPProvider", FakeProvider)
result = cli.execute_agent(agent_name="chef", input_text="hello", timeout_sec=5)
diff --git a/agentic-framework/tests/test_core_agents.py b/agentic-framework/tests/test_core_agents.py
index 35077f6..12562fe 100644
--- a/agentic-framework/tests/test_core_agents.py
+++ b/agentic-framework/tests/test_core_agents.py
@@ -11,12 +11,7 @@ async def ainvoke(self, payload, config):
return {"messages": [SimpleNamespace(content="done")]}
-def test_chef_agent_local_tools_and_prompt(monkeypatch):
- class FakeWebSearchTool:
- def invoke(self, query):
- return {"query": query}
-
- monkeypatch.setattr("agentic_framework.core.chef_agent.WebSearchTool", FakeWebSearchTool)
+def test_chef_agent_prompt_and_mcp(monkeypatch):
monkeypatch.setattr("agentic_framework.core.langgraph_agent.ChatOpenAI", lambda **kwargs: object())
monkeypatch.setattr("agentic_framework.core.langgraph_agent.create_agent", lambda **kwargs: DummyGraph())
@@ -24,7 +19,7 @@ def invoke(self, query):
result = asyncio.run(agent.run("ingredients"))
assert "personal chef" in agent.system_prompt
- assert len(agent.get_tools()) == 1
+ assert len(agent.get_tools()) == 0 # No local tools, uses MCP instead
assert result == "done"
diff --git a/agentic-framework/tests/test_mcp_config.py b/agentic-framework/tests/test_mcp_config.py
index 6dfcf0e..9be0678 100644
--- a/agentic-framework/tests/test_mcp_config.py
+++ b/agentic-framework/tests/test_mcp_config.py
@@ -2,27 +2,9 @@
def test_get_mcp_servers_config_returns_copy(monkeypatch):
- monkeypatch.delenv("TAVILY_API_KEY", raising=False)
- monkeypatch.delenv("TINYFISH_API_KEY", raising=False)
-
resolved = get_mcp_servers_config()
assert resolved.keys() == DEFAULT_MCP_SERVERS.keys()
assert resolved is not DEFAULT_MCP_SERVERS
- assert resolved["tavily"]["url"] == DEFAULT_MCP_SERVERS["tavily"]["url"]
-
-
-def test_get_mcp_servers_config_resolves_tavily_api_key(monkeypatch):
- monkeypatch.setenv("TAVILY_API_KEY", "secret")
-
- resolved = get_mcp_servers_config()
- assert "tavilyApiKey=secret" in resolved["tavily"]["url"]
-
-
-def test_get_mcp_servers_config_resolves_tinyfish_header(monkeypatch):
- monkeypatch.setenv("TINYFISH_API_KEY", "tiny-secret")
-
- resolved = get_mcp_servers_config()
- assert resolved["tinyfish"]["headers"]["X-API-Key"] == "tiny-secret"
def test_get_mcp_servers_config_applies_override():
diff --git a/agentic-framework/tests/test_registry.py b/agentic-framework/tests/test_registry.py
index de3e758..77d6d14 100644
--- a/agentic-framework/tests/test_registry.py
+++ b/agentic-framework/tests/test_registry.py
@@ -15,7 +15,7 @@ def test_registry_discovers_core_agents():
def test_registry_register_get_and_mcp_servers():
- @AgentRegistry.register("test-agent", mcp_servers=["tavily", "web-fetch"])
+ @AgentRegistry.register("test-agent", mcp_servers=["web-fetch"])
class TestAgent(Agent):
async def run(self, input_data, config=None):
return "ok"
@@ -25,7 +25,7 @@ def get_tools(self):
try:
assert AgentRegistry.get("test-agent") is TestAgent
- assert AgentRegistry.get_mcp_servers("test-agent") == ["tavily", "web-fetch"]
+ assert AgentRegistry.get_mcp_servers("test-agent") == ["web-fetch"]
assert "test-agent" in AgentRegistry.list_agents()
finally:
AgentRegistry._registry.pop("test-agent", None)
diff --git a/agentic-framework/tests/test_tools.py b/agentic-framework/tests/test_tools.py
index 738a022..dc117c1 100644
--- a/agentic-framework/tests/test_tools.py
+++ b/agentic-framework/tests/test_tools.py
@@ -1,5 +1,4 @@
from agentic_framework.tools.example import CalculatorTool, WeatherTool
-from agentic_framework.tools.web_search import WebSearchTool
def test_calculator_tool_success():
@@ -18,18 +17,6 @@ def test_weather_tool():
assert "Lisbon" in tool.invoke("Lisbon")
-def test_web_search_tool_calls_tavily(monkeypatch):
- class FakeTavilyClient:
- def search(self, query):
- return {"query": query, "results": ["a"]}
-
- monkeypatch.setattr("agentic_framework.tools.web_search.TavilyClient", lambda: FakeTavilyClient())
- tool = WebSearchTool()
- result = tool.invoke("langchain")
-
- assert result["query"] == "langchain"
-
-
def test_calculator_blocks_dangerous_operations():
tool = CalculatorTool()
# Test that function calls are blocked
diff --git a/agentic-framework/uv.lock b/agentic-framework/uv.lock
index 531fd65..4034cef 100644
--- a/agentic-framework/uv.lock
+++ b/agentic-framework/uv.lock
@@ -21,7 +21,6 @@ dependencies = [
{ name = "langchain-mcp-adapters" },
{ name = "langchain-model-profiles" },
{ name = "langchain-openai" },
- { name = "langchain-tavily" },
{ name = "langchain-text-splitters" },
{ name = "langgraph" },
{ name = "langgraph-api" },
@@ -29,8 +28,6 @@ dependencies = [
{ name = "langsmith" },
{ name = "mcp" },
{ name = "rich" },
- { name = "tavily" },
- { name = "tavily-python" },
{ name = "tree-sitter" },
{ name = "tree-sitter-languages" },
{ name = "typer" },
@@ -56,7 +53,6 @@ requires-dist = [
{ name = "langchain-mcp-adapters", specifier = ">=0.1.13" },
{ name = "langchain-model-profiles", specifier = ">=0.0.4" },
{ name = "langchain-openai", specifier = ">=1.1.1" },
- { name = "langchain-tavily", specifier = ">=0.2.13" },
{ name = "langchain-text-splitters", specifier = ">=1.0.0" },
{ name = "langgraph", specifier = ">=1.0.3" },
{ name = "langgraph-api", specifier = ">=0.5.37" },
@@ -64,8 +60,6 @@ requires-dist = [
{ name = "langsmith", specifier = ">=0.4.43" },
{ name = "mcp", specifier = ">=1.21.1" },
{ name = "rich", specifier = ">=13.0.0" },
- { name = "tavily", specifier = ">=1.1.0" },
- { name = "tavily-python", specifier = ">=0.7.13" },
{ name = "tree-sitter", specifier = "==0.21.3" },
{ name = "tree-sitter-languages", specifier = ">=1.10.2" },
{ name = "typer", specifier = ">=0.12.0" },
@@ -1275,21 +1269,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/a1/50e7596aca775d8c3883eceeaf47489fac26c57c1abe243c00174f715a8a/langchain_openai-1.1.7-py3-none-any.whl", hash = "sha256:34e9cd686aac1a120d6472804422792bf8080a2103b5d21ee450c9e42d053815", size = 84753, upload-time = "2026-01-07T19:44:58.629Z" },
]
-[[package]]
-name = "langchain-tavily"
-version = "0.2.17"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp" },
- { name = "langchain" },
- { name = "langchain-core" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e6/32/f7b5487efbcd5fca5d4095f03dce7dcf0301ed81b2505d9888427c03619b/langchain_tavily-0.2.17.tar.gz", hash = "sha256:738abd790c50f19565023ad279c8e47e87e1aeb971797fec30a614b418ae6503", size = 25298, upload-time = "2026-01-18T13:09:04.112Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/35/f9/bb6f1cea2a19215e4169a3bcec3af707ff947cf62f6ef7d28e7280f03e29/langchain_tavily-0.2.17-py3-none-any.whl", hash = "sha256:da4e5e7e328d054dc70a9c934afa1d1e62038612106647ff81ad8bfbe3622256", size = 30734, upload-time = "2026-01-18T13:09:03.1Z" },
-]
-
[[package]]
name = "langchain-tests"
version = "1.1.4"
@@ -2667,30 +2646,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214, upload-time = "2025-03-24T01:36:35.278Z" },
]
-[[package]]
-name = "tavily"
-version = "1.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp" },
- { name = "requests" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/63/ba/cd74acdb0537a02fb5657afbd5fd5a27a298c85fc27f544912cc001377bb/tavily-1.1.0.tar.gz", hash = "sha256:7730bf10c925dc0d0d84f27a8979de842ecf88c2882183409addd855e27d8fab", size = 5081, upload-time = "2025-10-31T09:32:40.555Z" }
-
-[[package]]
-name = "tavily-python"
-version = "0.7.21"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "httpx" },
- { name = "requests" },
- { name = "tiktoken" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ff/1f/9d5c4ca7034754d1fc232af64638b905162bdf3012e9629030e3d755856f/tavily_python-0.7.21.tar.gz", hash = "sha256:897bedf9b1c2fad8605be642e417d6c7ec1b79bf6199563477cf69c4313f824a", size = 21813, upload-time = "2026-01-30T16:57:33.186Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/39/85e5be4e9a912022f86f38288d1f4dd2d100b60ec75ebf3da37ca0122375/tavily_python-0.7.21-py3-none-any.whl", hash = "sha256:acfb5b62f2d1053d56321b4fb1ddfd2e98bb975cc4446b86b3fe2d3dd0850288", size = 17957, upload-time = "2026-01-30T16:57:32.278Z" },
-]
-
[[package]]
name = "tenacity"
version = "9.1.3"
diff --git a/docs/resources/demo.cast b/docs/resources/demo.cast
new file mode 100644
index 0000000..4d1916f
--- /dev/null
+++ b/docs/resources/demo.cast
@@ -0,0 +1,17 @@
+{"version": 2, "width": 188, "height": 53, "timestamp": 1772224061, "idle_time_limit": 2.0, "env": {"SHELL": "/bin/zsh", "TERM": "tmux-256color"}}
+[3.478129, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
+[3.519396, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;214;93;14m\u001b[48;2;214;93;14;38;2;251;241;199m jeancsil \u001b[48;2;215;153;33;38;2;214;93;14m\u001b[38;2;251;241;199m …/agents \u001b[48;2;104;157;106;38;2;215;153;33m\u001b[38;2;251;241;199m gitbutler/workspace !? \u001b[48;2;69;133;136;38;2;104;157;106m\u001b[48;2;102;92;84;38;2;69;133;136m\u001b[38;2;131;165;152m orbstack \u001b[48;2;60;56;54;38;2;102;92;84m\u001b[0m\u001b[38;2;60;56;54m \u001b[0m\r\n\u001b[1;38;2;152;151;26m\u001b[0m \u001b[K\u001b[?2004h"]
+[10.50343, "o", " bin/agent.sh -v travel-coordinator --input \"Short trip from BCN to Lisbon from 27/06 to 29/06/2026.\""]
+[10.515468, "o", "\u001b[101D \u001b[32mb\u001b[32mi\u001b[32mn\u001b[32m/\u001b[32ma\u001b[32mg\u001b[32me\u001b[32mn\u001b[32mt\u001b[32m.\u001b[32ms\u001b[32mh\u001b[39m\u001b[31C\u001b[33m\"\u001b[33mS\u001b[33mh\u001b[33mo\u001b[33mr\u001b[33mt\u001b[33m \u001b[33mt\u001b[33mr\u001b[33mi\u001b[33mp\u001b[33m \u001b[33mf\u001b[33mr\u001b[33mo\u001b[33mm\u001b[33m \u001b[33mB\u001b[33mC\u001b[33mN\u001b[33m \u001b[33mt\u001b[33mo\u001b[33m \u001b[33mL\u001b[33mi\u001b[33ms\u001b[33mb\u001b[33mo\u001b[33mn\u001b[33m \u001b[33mf\u001b[33mr\u001b[33mo\u001b[33mm\u001b[33m \u001b[33m2\u001b[33m7\u001b[33m/\u001b[33m0\u001b[33m6\u001b[33m \u001b[33mt\u001b[33mo\u001b[33m \u001b[33m2\u001b[33m9\u001b[33m/\u001b[33m0\u001b[33m6\u001b[33m/\u001b[33m2\u001b[33m0\u001b[33m2\u001b[33m6\u001b[33m.\u001b[33m\"\u001b[39m"]
+[12.220225, "o", "\u001b[?2004l\r\r\n"]
+[12.680269, "o", "\u001b[0;34mRunning:\u001b[0m agentic-run --verbose travel-coordinator --input Short trip from BCN to Lisbon from 27/06 to 29/06/2026.\r\n\r\n"]
+[14.297753, "o", "\u001b[1;34mRunning agent:\u001b[0m travel-coordinator\u001b[33m...\u001b[0m\r\n"]
+[97.109799, "o", "\u001b[1;32mResult from travel-coordinator:\u001b[0m\r\n"]
+[97.126459, "o", "\r\n# **Final Itinerary Recommendation**\r\n\r\n## \u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m Recommended Itinerary\r\n\r\n**Primary Recommendation**: €\u001b[1;36m82\u001b[0m option\r\n- **Outbound**: BCN → LIS on \u001b[1;36m27\u001b[0m/\u001b[1;36m06\u001b[0m/\u001b[1;36m2026\u001b[0m at \u001b[1;92m18:15\u001b[0m → \u001b[1;92m19:20\u001b[0m \u001b[1m(\u001b[0m2h 5m\u001b[1m)\u001b[0m\r\n- **Return**: LIS → BCN on \u001b[1;36m29\u001b[0m/\u001b[1;36m06\u001b[0m/\u001b[1;36m2026\u001b[0m at \u001b[1;92m18:55\u001b[0m → \u001b[1;92m21:50\u001b[0m \u001b[1m(\u001b[0m1h 55m\u001b[1m)\u001b[0m\r\n- **Total Cost**: €\u001b[1;36m82\u001b[0m round trip\r\n- **Booking Link**: \u001b[1m(\u001b[0m\u001b[4;94mhttps://on.kiwi.com/jGBAIf\u001b[0m\u001b[4;94m)\u001b[0m\r\n\r\n## \u001b[1;36m2\u001b[0m\u001b[1m)\u001b[0m Why This Option\r\n\r\nThis recommendation offers the **best value proposition** for a short weekend getaway:\r\n\r\n- **Optimal Timing**: Evening departure allows for a full day in Barcelona before traveling, while the \u001b[1;92m19:20\u001b[0m arrival in Lisbon gives you almost a full evening to settle in and explore\r\n- **Budget-Friendly**: At €\u001b[1;36m82\u001b[0m, this is significantly cheaper than the shortest flight options \u001b[1m(\u001b[0m€\u001b[1;36m115\u001b[0m\u001b[1m)\u001b"]
+[97.126666, "o", "[0m and represents the lowest available price\r\n- **Convenient Return**: The \u001b[1;92m18:55\u001b[0m departure from Lisbon gets you back to Barcelona by \u001b[1;92m21:50\u001b[0m, perfect for those who need to work the next day or prefer earlier returns\r\n- **Direct Flights**: Both legs are direct \u001b[1;36m2\u001b[0m-hour flights, maximizing your time in Lisbon\r\n- **Good Availability**: Multiple return time options if preferred timing changes\r\n\r\n## \u001b[1;36m3\u001b[0m\u001b[1m)\u001b[0m Risks \u001b[35m/\u001b[0m Caveats\r\n\r\n- **Limited Evening Time**: The \u001b[1;92m18:15\u001b[0m departure means you'll need to finish any Barcelona activities before then\r\n- **Return Flight Timing**: The \u001b[1;92m18:55\u001b[0m return may limit your final day in Lisbon - consider if this aligns with your departure plans\r\n- **No Buffer Time**: Flights are tightly scheduled with minimal flexibility; delays could impact connections \u001b[1m(\u001b[0mthough these are direct flights\u001b[1m)\u001b[0m\r\n- **Summer Travel**: Late June is peak season; flights may be busier and potentially more susceptible to delays\r\n\r\n#"]
+[97.126741, "o", "# \u001b[1;36m4\u001b[0m\u001b[1m)\u001b[0m Next Best Alternative\r\n\r\nIf the timing doesn't work or you prefer a later departure:\r\n\r\n- **Alternative Option**: BCN → LIS on \u001b[1;36m27\u001b[0m/\u001b[1;36m06\u001b[0m at \u001b[1;92m18:15\u001b[0m → \u001b[1;92m19:20\u001b[0m, return on \u001b[1;36m29\u001b[0m/\u001b[1;36m06\u001b[0m at \u001b[1;92m22:35\u001b[0m → \u001b[1;92m01:30\u001b[0m+\u001b[1;36m1\u001b[0m for the same €\u001b[1;36m82\u001b[0m\r\n- **Benefits**: More time in Lisbon on your departure day, evening return\r\n- **Considerations**: Late night arrival in Barcelona, may require accommodation adjustment\r\n\r\n**Note**: All flights are direct \u001b[1;36m2\u001b[0m-hour flights, so the main differentiator is timing and price. The €\u001b[1;36m82\u001b[0m options represent the sweet spot between cost and convenience for this short \r\nweekend trip. Have a wonderful time exploring Lisbon! 🇵🇹\r\n"]
+[97.643393, "o", "\r\n\u001b[0;32m✓ Command completed successfully\u001b[0m\r\n"]
+[97.643462, "o", "\u001b[0;34mLogs:\u001b[0m /Users/jeancsil/code/agents/agentic-framework/logs/agent.log\r\n"]
+[97.644172, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
+[97.697211, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;214;93;14m\u001b[48;2;214;93;14;38;2;251;241;199m jeancsil \u001b[48;2;215;153;33;38;2;214;93;14m\u001b[38;2;251;241;199m …/agents \u001b[48;2;104;157;106;38;2;215;153;33m\u001b[38;2;251;241;199m gitbutler/workspace !? \u001b[48;2;69;133;136;38;2;104;157;106m\u001b[48;2;102;92;84;38;2;69;133;136m\u001b[38;2;131;165;152m orbstack \u001b[48;2;60;56;54;38;2;102;92;84m\u001b[0m\u001b[38;2;60;56;54m \u001b[0m\r\n\u001b[1;38;2;152;151;26m\u001b[0m \u001b[K\u001b[?2004h"]
+[99.022821, "o", "\u001b[?2004l\r\r\n"]