From e22f192feaf38a7bbf6a085d8d6a6be1a8830e50 Mon Sep 17 00:00:00 2001 From: Jean Silva Date: Fri, 27 Feb 2026 23:30:41 +0100 Subject: [PATCH] feat: remove paid APIs (Tavily, Tinyfish) and use free MCP alternatives - Remove Tavily dependencies (langchain-tavily, tavily-python, tavily) from pyproject.toml - Remove web_search.py Tavily-based tool - Update chef_agent.py to use web-fetch MCP server instead of local web_search tool - Remove Tavily and Tinyfish from MCP server configuration - Remove TAVILY_API_KEY and TINYFISH_API_KEY from .env.example - Update README.md to reflect changes to MCP servers and local tools - Update TODO.md to remove API keys section - Update tests to reflect new architecture All tests pass with 80% coverage maintained. Co-Authored-By: Claude Opus 4.6 --- .env.example | 3 - README.md | 9 +- .../PLAN_FILE_EDITING_IMPROVEMENTS.md | 401 ++++++++++++++++++ agentic-framework/TODO.md | 7 - agentic-framework/pyproject.toml | 3 - .../src/agentic_framework/core/chef_agent.py | 20 +- .../src/agentic_framework/mcp/config.py | 32 +- .../src/agentic_framework/tools/__init__.py | 2 - .../src/agentic_framework/tools/web_search.py | 25 -- agentic-framework/tests/test_cli.py | 2 +- agentic-framework/tests/test_core_agents.py | 9 +- agentic-framework/tests/test_mcp_config.py | 18 - agentic-framework/tests/test_registry.py | 4 +- agentic-framework/tests/test_tools.py | 13 - agentic-framework/uv.lock | 45 -- docs/resources/demo.cast | 17 + 16 files changed, 430 insertions(+), 180 deletions(-) create mode 100644 agentic-framework/PLAN_FILE_EDITING_IMPROVEMENTS.md delete mode 100644 agentic-framework/src/agentic_framework/tools/web_search.py create mode 100644 docs/resources/demo.cast 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"]