Sitemap
Krishnapankhania

Krishnapankhania

I Built an Autonomous AI Agent from Scratch in Python — Here’s Exactly How

8 min readJun 2, 2026

--

A step-by-step guide to building a multi-tool AI agent with Groq, LLaMA 3.3, DuckDuckGo Search, and a Streamlit UI — no LangChain required.

What you’ll build: A fully functional AI agent that can search the web in real time, read files, hold multi-turn conversations, and run inside a sleek dark-themed chat UI. The kind of project that turns “LLM API integration” on your resume into something you can actually demo.

Why I Built This

Everyone talks about AI agents. But most tutorials either hand you a LangChain abstraction that hides all the interesting parts, or they’re so theoretical you can’t run the code.

I wanted to build one raw — understand exactly how the agent loop works, how tool calling is orchestrated, and how to wrap it in a UI someone can actually use.

This is that guide.

What We’re Building

A Python-based AI agent with:

  • Tool 1 — Web Search: Queries DuckDuckGo and returns live results
  • Tool 2 — File Reader: Reads .txt files from disk
  • Agent Brain: LLaMA 3.3 70B via Groq API, deciding when to use tools
  • Memory: Full multi-turn conversation history
  • UI: Streamlit dark-themed chat interface with tool usage badges

Stack: Python · Groq API · LLaMA 3.3 70B · DuckDuckGo Search · Streamlit

How an AI Agent Actually Works

Before we write a single line, let’s understand the loop:

User asks a question
↓
Agent (LLM) decides: do I need a tool?
↓
If yes → call the tool → get result → feed back to LLM
↓
If no → answer directly
↓
Return answer to user

The key insight is that the LLM doesn’t execute tools — it decides to use them by returning structured JSON. Your Python code reads that JSON and actually runs the tool. This is the foundation of every agent framework out there.

Setup

Open your terminal:

mkdir ai-agent
cd ai-agent
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # Mac/Linux
pip install groq duckduckgo-search streamlit

You’ll need a free Groq API key from console.groq.com. Groq runs LLaMA 3.3 70B with extremely fast inference — perfect for an agent loop that may call the LLM multiple times per query.

Part 1 — Tool 1: Web Search

Create agent.py and add:

from duckduckgo_search import DDGS
import json
from groq import Groq
def search_web(query: str) -> str:
"""Search the web and return top 3 results"""
with DDGS() as ddgs:
results = list(ddgs.text(query, max_results=3))

if not results:
return "No results found."

output = ""
for r in results:
output += f"Title: {r['title']}\n"
output += f"Summary: {r['body']}\n"
output += f"URL: {r['href']}\n\n"

return output

DuckDuckGo’s search library requires no API key and returns real-time results. We format each result with its title, summary snippet, and URL so the LLM has full context to answer from.

Part 2 — Tool 2: File Reader

def read_file(filename: str) -> str:
"""Read a text file and return its content"""
try:
if filename.endswith(".txt"):
with open(filename, "r") as f:
return f.read()
else:
return "Error: Only .txt files supported for now"
except FileNotFoundError:
return f"Error: File '{filename}' not found"

Simple but powerful. The agent can now answer questions about any .txt file you drop in the project folder — meeting notes, research papers, config files, anything.

Part 3 — The Agent Brain

This is the core of the whole project. Here’s what’s happening:

  1. We describe the available tools to the LLM in the system prompt
  2. We instruct it to always respond in JSON format
  3. We loop up to 3 times — each loop it either calls a tool or gives a final answer
client = Groq(api_key="your_groq_api_key_here")
TOOLS_DESCRIPTION = """
You are a helpful AI assistant with access to these tools:
1. search_web(query) — Search the internet for current information
2. read_file(filename) — Read content from a text file
To use a tool, respond ONLY in this JSON format:
{"tool": "search_web", "input": "your search query"}
{"tool": "read_file", "input": "filename.txt"}
If you don't need a tool, respond ONLY in this JSON format:
{"tool": "none", "answer": "your direct answer here"}
Always use a tool when the question needs current information or file content.
"""
def run_tool(tool_name: str, tool_input: str) -> str:
if tool_name == "search_web":
return search_web(tool_input)
elif tool_name == "read_file":
return read_file(tool_input)
else:
return "Unknown tool"
def agent(user_question: str, conversation_history: list) -> str:
conversation_history.append({
"role": "user",
"content": user_question
})

for i in range(3): # max 3 iterations
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
max_tokens=500,
messages=[
{"role": "system", "content": TOOLS_DESCRIPTION}
] + conversation_history
)

ai_response = response.choices[0].message.content.strip()

try:
parsed = json.loads(ai_response)
except json.JSONDecodeError:
conversation_history.append({
"role": "assistant", "content": ai_response
})
return ai_response

if parsed.get("tool") == "none":
answer = parsed.get("answer", "I don't know.")
conversation_history.append({
"role": "assistant", "content": answer
})
return answer

# Tool call
tool_name = parsed.get("tool")
tool_input = parsed.get("input")

print(f"\n🔧 Using tool: {tool_name}({tool_input})")
tool_result = run_tool(tool_name, tool_input)

conversation_history.append({
"role": "assistant",
"content": f"Tool used: {tool_name}\nResult: {tool_result}"
})
conversation_history.append({
"role": "user",
"content": "Now answer my original question based on this result."
})

return "I could not find a good answer after searching."

The conversation_history list is what gives the agent memory. Every turn — including tool results — gets appended to the history and sent with the next API call. The LLM always has full context of everything that happened.

Part 4 — CLI Entry Point

Add this at the bottom of agent.py to run it in your terminal:

if __name__ == "__main__":
print("🤖 AI Agent Ready! Type 'quit' to exit\n")

conversation_history = []

while True:
user_input = input("You: ").strip()

if user_input.lower() == "quit":
break

if not user_input:
continue

print("\n🤔 Agent thinking...\n")
answer = agent(user_input, conversation_history)
print(f"Agent: {answer}\n")

Run it:

python agent.py

You should see something like this in your terminal:

🤖 AI Agent Ready! Type 'quit' to exit
You: latest AI news 2026
🤔 Agent thinking...
🔧 Using tool: search_web(latest AI news 2026)
Agent: The latest AI news includes the release of Gemini Omni and Gemini 3.5 by Google...
You: Explain Machine Learning in simple words
🤔 Agent thinking...
Agent: Machine Learning is a type of AI that allows computers to learn from data...

Notice how the agent knew to search the web for current news, but answered the ML explanation directly without using a tool. That’s the LLM making intelligent routing decisions in real time.

Part 5 — The Streamlit UI

Now let’s give it a proper frontend. Create a new file app.py:

import streamlit as st
import json
from groq import Groq
from duckduckgo_search import DDGS
st.set_page_config(page_title="AI Agent", page_icon="🤖", layout="centered")st.markdown("""
<style>
.stApp { background-color: #0f0f0f; }
.hero { text-align: center; padding: 2rem 0 1rem 0; }
.hero h1 {
font-size: 2.8rem; font-weight: 700;
background: linear-gradient(135deg, #a78bfa, #60a5fa, #34d399);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
}
.hero p { color: #6b7280; font-size: 1rem; }
.capability-bar {
display: flex; justify-content: center;
gap: 12px; margin: 1rem 0 2rem 0; flex-wrap: wrap;
}
.cap-pill {
background: #1a1a2e; border: 1px solid #2d2d4e;
border-radius: 999px; padding: 6px 16px;
font-size: 0.78rem; color: #a78bfa;
}
.chat-bubble-user {
background: linear-gradient(135deg, #4f46e5, #7c3aed);
color: white; padding: 12px 18px;
border-radius: 18px 18px 4px 18px;
margin: 8px 0; max-width: 80%; margin-left: auto;
box-shadow: 0 4px 15px rgba(124, 58, 237, 0.3);
}
.chat-bubble-agent {
background: #1a1a1a; border: 1px solid #2a2a2a;
color: #e5e7eb; padding: 12px 18px;
border-radius: 18px 18px 18px 4px;
margin: 8px 0; max-width: 85%; line-height: 1.6;
}
.tool-badge {
display: inline-block; background: #0d2318;
border: 1px solid #14532d; color: #4ade80;
border-radius: 8px; padding: 4px 12px;
font-size: 0.75rem; font-family: monospace; margin-bottom: 8px;
}
.status-dot {
display: inline-block; width: 8px; height: 8px;
background: #4ade80; border-radius: 50%;
margin-right: 6px; animation: pulse 2s infinite;
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
.footer-bar {
text-align: center; color: #374151; font-size: 0.75rem;
padding: 1rem 0; border-top: 1px solid #1f1f1f; margin-top: 2rem;
}
</style>
""", unsafe_allow_html=True)
st.markdown("""
<div class="hero">
<h1>🤖 AI Agent</h1>
<p><span class="status-dot"></span>Online · Powered by Groq LLaMA 3.3 70B</p>
</div>
<div class="capability-bar">
<span class="cap-pill">🔍 Web Search</span>
<span class="cap-pill">📄 File Reader</span>
<span class="cap-pill">🧠 Multi-turn Memory</span>
<span class="cap-pill">⚡ Fast Inference</span>
</div>
""", unsafe_allow_html=True)
if "history" not in st.session_state:
st.session_state.history = []
if "messages" not in st.session_state:
st.session_state.messages = []
if "tool_logs" not in st.session_state:
st.session_state.tool_logs = {}
# (paste the full agent_ui() function and render loop from above here)st.markdown("""
<div class="footer-bar">
Built with Streamlit · Groq · LLaMA 3.3 · DuckDuckGo Search
</div>
""", unsafe_allow_html=True)

Run it:

streamlit run app.py

The Architecture at a Glance

User Input
│
▼
┌─────────────────────────────┐
│ agent() │
│ │
│ 1. Append to history │
│ 2. Call Groq LLM │
│ 3. Parse JSON response │
│ │
│ ┌──────────┐ ┌─────────┐ │
│ │search_web│ │read_file│ │
│ └──────────┘ └─────────┘ │
│ │
│ 4. Append tool result │
│ 5. Loop (max 3x) │
└─────────────────────────────┘
│
▼
Final Answer → Streamlit UI

Key Concepts You Just Learned

1. The ReAct Pattern This agent uses a simplified version of the ReAct (Reason + Act) pattern. The LLM reasons about what to do, acts by calling a tool, observes the result, then reasons again. This loop is the backbone of every production agent.

Get Krishnapankhania’s stories in your inbox

Join Medium for free to get updates from this writer.

2. Prompt Engineering for Tool Use The entire tool-calling system is driven by a system prompt. The LLM has no native ability to call functions — we teach it to output structured JSON and our Python code does the rest. This is exactly how early ChatGPT plugins worked.

3. Conversation History as Memory There’s no fancy memory system here. The conversation_history list is literally everything the agent knows. Every message — user, assistant, tool results — gets appended and resent each turn. Simple and effective.

4. JSON as an Interface By asking the LLM to respond only in JSON, we create a clean interface between the LLM’s reasoning and our Python execution. This is a fundamental pattern in LLM application development.

What to Build Next

Now that you have the foundation, here’s how to extend it:

  • Add PDF support — use PyMuPDF in read_file() to handle .pdf files
  • Add a calculator tool — let the agent evaluate math expressions with eval()
  • Add persistent memory — save conversation_history to a JSON file between sessions
  • Deploy to Streamlit Cloud — free hosting, live URL for your resume in under 5 minutes
  • Add more LLM providers — swap Groq for Claude or GPT-4 by changing 3 lines

Final Project Structure

ai-agent/
├── agent.py ← tools + agent brain + CLI
├── app.py ← Streamlit UI
├── venv/ ← virtual environment
└── (any .txt files you want the agent to read)

Wrapping Up

What we built here is genuinely non-trivial. You have a working agent loop, real-time web access, file reading, multi-turn memory, and a polished UI — all in under 300 lines of Python.

More importantly, you understand why every piece is there. When you read about LangChain agents, AutoGPT, or Claude’s tool use now, you’ll recognise the exact patterns you just built by hand.

That understanding is worth more than any framework shortcut.

If this helped you, follow me for more hands-on AI engineering tutorials. Full source code available on GitHub.

Press enter or click to view image in full size
Press enter or click to view image in full size
Press enter or click to view image in full size

--

--