An agent that can only process text is fundamentally limited. Real usefulness comes from connecting to external systems - fetching live data, querying databases, calling APIs, and triggering actions in the real world. In this post, I’ll explore how to build these connections, turning isolated language models into integrated systems that can actually get things done.
The Integration Challenge
LLMs excel at understanding and generating text, but they operate in a vacuum. They don’t know:
Today’s weather or stock prices
Your company’s current inventory
The user’s calendar events
What happened after their training cutoff
Tools bridge this gap, but designing reliable integrations requires careful thought about authentication, error handling, rate limiting, and data transformation.
flowchart LR
A[Agent] --> T{Tool Layer}
T --> W[Web Search]
T --> D[Databases]
T --> API[External APIs]
T --> S[Services]
W --> R[Results]
D --> R
API --> R
S --> R
R --> A
style A fill:#fff3e0
style T fill:#e3f2fd
Web Search Integration
Web search gives agents access to current information beyond their training data. Here’s how to integrate search capabilities:
from langchain_community.tools import TavilySearchResults from langchain_core.tools import tool
# Using Tavily (optimized for LLM use) search_tool = TavilySearchResults(max_results=5)
@tool defweb_search(query: str) -> str: """ Search the web for current information. Args: query: The search query Returns: Search results as formatted text """ results = search_tool.invoke({"query": query})
# Format results for the LLM formatted = [] for result in results: formatted.append(f"Title: {result['title']}") formatted.append(f"URL: {result['url']}") formatted.append(f"Content: {result['content'][:500]}") formatted.append("---")
classResearchAgent: def__init__(self): self.tools = [web_search] self.tool_map = {t.name: t for t inself.tools}
defresearch(self, topic: str) -> str: messages = [ { "role": "system", "content": """You are a research assistant. Use web search to find current, accurate information. Always cite sources.""" }, {"role": "user", "content": f"Research this topic: {topic}"} ]
@tool defquery_database(sql: str) -> str: """ Execute a read-only SQL query against the database. Args: sql: SELECT query to execute (no modifications allowed) Returns: Query results as formatted text """ # Safety check - only allow SELECT ifnot sql.strip().upper().startswith("SELECT"): return"Error: Only SELECT queries are allowed"
try: conn = sqlite3.connect("company.db") cursor = conn.cursor() cursor.execute(sql) columns = [desc[0] for desc in cursor.description] rows = cursor.fetchall() conn.close()
# Format as readable table result = " | ".join(columns) + "\n" result += "-" * 40 + "\n" for row in rows: result += " | ".join(str(v) for v in row) + "\n"
return result
except Exception as e: returnf"Query error: {str(e)}"
@tool defget_schema() -> str: """ Get the database schema to understand available tables and columns. Returns: Schema description """ conn = sqlite3.connect("company.db") cursor = conn.cursor()
cursor.execute( "SELECT name FROM sqlite_master WHERE type='table'" ) tables = cursor.fetchall()
schema = [] for (table_name,) in tables: cursor.execute(f"PRAGMA table_info({table_name})") columns = cursor.fetchall() col_info = [f"{col[1]} ({col[2]})"for col in columns] schema.append(f"{table_name}: {', '.join(col_info)}")
conn.close() return"\n".join(schema)
Natural Language to SQL
Enable agents to translate natural language to SQL:
# Create tools from API client weather_client = APIClient( base_url="https://api.weather.example.com", api_key=os.environ["WEATHER_API_KEY"] )
@tool defget_weather(city: str) -> dict: """ Get current weather for a city. Args: city: City name Returns: Weather data including temperature and conditions """ try: return weather_client.get("current", params={"city": city}) except requests.RequestException as e: return {"error": f"Failed to fetch weather: {str(e)}"}
@tool defread_file(file_path: str) -> str: """ Read contents of a text file. Args: file_path: Path to the file to read Returns: File contents """ path = Path(file_path)
# Security: restrict to allowed directories allowed_dirs = [Path("./data"), Path("./documents")] ifnotany(path.resolve().is_relative_to(d.resolve()) for d in allowed_dirs): return"Error: Access denied - path outside allowed directories"
ifnot path.exists(): returnf"Error: File not found: {file_path}"
@tool defwrite_file(file_path: str, content: str) -> str: """ Write content to a text file. Args: file_path: Path to the file to write content: Content to write Returns: Success or error message """ path = Path(file_path)
# Security check allowed_dirs = [Path("./output")] ifnotany(path.resolve().is_relative_to(d.resolve()) for d in allowed_dirs): return"Error: Access denied - can only write to output directory"
try: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content) returnf"Successfully wrote to {file_path}" except Exception as e: returnf"Error writing file: {str(e)}"
Building a Multi-Tool Agent
Combining multiple integrations into a capable agent:
classIntegratedAgent: def__init__(self): self.tools = [ web_search, query_database, get_schema, get_weather, read_file, write_file ] self.tool_map = {t.name: t for t inself.tools}
defrun(self, task: str) -> str: messages = [ { "role": "system", "content": """You are a capable assistant with access to multiple tools: - web_search: Find current information online - query_database/get_schema: Query company database - get_weather: Get weather information - read_file/write_file: Work with files Use tools when needed. Chain multiple tools for complex tasks. Always explain what you're doing and why.""" }, {"role": "user", "content": task} ]
for tool_call in message.tool_calls: result = self._execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result })
return"Reached maximum iterations without completing task"
Layer your integrations: Separate tool definitions from business logic and error handling
Always handle failures: Network calls fail, APIs rate limit, databases time out
Security is non-negotiable: Validate inputs, restrict access, protect credentials
Provide context to LLMs: Format external data clearly so the model can use it effectively
Log everything: External calls should be logged for debugging and monitoring
With external connections, agents transform from text processors into capable systems that can research, query, and act. In the next post, I’ll explore agentic RAG - dynamically retrieving information to augment agent knowledge - and strategies for evaluating agent performance.
This is Part 10 of my series on building intelligent AI systems. Next: agentic RAG and agent evaluation strategies.
Comments