How to Build Your First AI, GenAI & Agentic AI Project
Every tutorial on the internet right now starts with a ten-paragraph essay defining what an LLM is, as if you haven't been forced to use GitHub Copilot or ChatGPT for the last two years. Let's skip that.
The problem isn't understanding what Generative AI is anymore. The problem is that when you sit down to actually build something beyond a wrapper that sends "Hello" to the OpenAI API, the terminology gets murky fast. What makes something "GenAI"? When does an app cross the line into being an "Agent"?
We are going to build a small local CLI tool that takes a messy, unstructured text file of raw server logs, generates a summary using an LLM, and then—here's the agentic part—automatically writes and executes a cleanup script based on what it found.
No LangChain abstractions that hide what's actually happening. Just Python, the raw openai library (pointing to whatever model you have), and standard subprocess calls.
1. Setting up the environment (and dodging the framework bloat)
When I first started looking at agentic workflows, I tried LangChain first. It was more trouble than it was worth. I spent three hours debugging why some abstracted chain was eating my prompt variables before realizing the documentation I was reading was for a version released three weeks prior that was already deprecated.
We are going to write plain Python.
Create a new directory and set up a virtual environment. We'll need the OpenAI SDK and python-dotenv for managing API keys. If you don't want to pay OpenAI for messing around, swap out the base URL for a local Ollama instance running llama3 or mistral. The code works the same either way.
mkdir log-agent
cd log-agent
python3 -m venv venv
source venv/bin/activate
pip install openai python-dotenv
Create a .env file in the root:
OPENAI_API_KEY=your_key_here
# Or if using Ollama locally:
# OPENAI_BASE_URL=http://localhost:11434/v1
# OPENAI_API_KEY=ollama
Now let's create a dummy log file to work with. Call it server.log:
[2026-03-05 10:12:01] INFO: Server started on port 8080
[2026-03-05 10:14:32] ERROR: Connection refused to database at 10.0.0.4:5432
[2026-03-05 10:15:00] WARNING: High memory usage detected: 92%
[2026-03-05 10:20:12] ERROR: Out of disk space on /var/log. Current free: 0MB
[2026-03-05 10:21:00] INFO: Attempting log rotation... failed.
2. Moving from GenAI to Agentic AI (Function Calling)
Standard Generative AI takes an input and generates text. You give it the log file, it writes a nice summary. That's neat, but it still requires a human to read the summary and fix the problem.
Agentic AI closes the loop. It gives the model the ability to take actions based on what it generates. We do this using tool use (sometimes called function calling). We will define a Python function that can run shell commands, tell the LLM about it, and let the model decide if it needs to run it.
Here is the core script. Save this as agent.py:
import os
import json
import subprocess
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI()
# Define the tool our agent is allowed to use
def execute_cleanup_command(command: str) -> str:
"""Executes a bash command to clean up disk space or restart services."""
print(f"\n[AGENT ACTION] About to run command: {command}")
confirmation = input("Allow this action? (y/n): ")
if confirmation.lower() != 'y':
return "Action denied by user."
try:
result = subprocess.run(
command, shell=True, check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)
return f"Success:\n{result.stdout}"
except subprocess.CalledProcessError as e:
return f"Error executing command:\n{e.stderr}"
# Map the function name to the actual callable
available_tools = {
"execute_cleanup_command": execute_cleanup_command
}
def run_log_agent(log_path: str):
if not os.path.exists(log_path):
print(f"File not found: {log_path}")
return
with open(log_path, 'r') as f:
log_content = f.read()
system_prompt = """
You are a DevOps assistant. Analyze the provided server logs.
If you find critical issues like disk space errors, you have access to a tool
to execute bash commands to fix them. Only use the tool if strictly necessary.
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Here are the logs to analyze:\n\n{log_content}"}
]
# Describe the tool to the LLM
tools = [
{
"type": "function",
"function": {
"name": "execute_cleanup_command",
"description": "Run a safe shell command to resolve server issues like clearing logs or restarting services.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"name": "command",
"description": "The bash command to execute (e.g., 'truncate -s 0 /var/log/*.log')"
}
},
"required": ["command"]
}
}
}
]
print("Analyzing logs with LLM...")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
tool_choice="auto"
)
response_message = response.choices[0].message
messages.append(response_message)
# Check if the model wants to call a function
if response_message.tool_calls:
for tool_call in response_message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.arguments)
if function_name in available_tools:
tool_output = available_tools[function_name](
command=function_args.get("command")
)
# Send the tool results back to the model
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": function_name,
"content": tool_output
})
# Get the final response from the model after the tool execution
second_response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages
)
print("\nFinal Agent Response:")
print(second_response.choices[0].message.content)
else:
print("\nAgent Response:")
print(response_message.content)
if __name__ == "__main__":
run_log_agent("server.log")
Run it with:
python agent.py
3. The part where things break (Gotchas and failure modes)
If you run the script above, it will likely spot the Out of disk space on /var/log error and try to run a command to clear space. That's the cool part working.
Here is what will trip you up in practice, because it definitely tripped me up: The model hallucinating destructive commands.
When I first tested a variation of this agent, I gave it permission to run generic shell commands without strict system prompt guardrails. Instead of safely clearing a log file, it tried to run rm -rf /var/log/* without checking directory paths properly.
Another subtle issue is silent parameter malformation. If you use smaller local models via Ollama (like Llama 3 8B), they sometimes mess up the JSON schema for tool arguments. You'll get an error like:
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
This happens because the model tried to output markdown code blocks inside the tool arguments field instead of raw JSON. If you're building real agents, you must wrap tool execution in try/except blocks and build a retry loop when the JSON parsing fails. Never trust the LLM to format output correctly 100% of the time.
This is also why line 14 of our script has an explicit input() confirmation check. Never give an autonomous agent unmonitored shell access. Even if the prompt says "be careful," models drift, especially in long loops.
Next Steps
Take this script and add a while loop so the agent can run in a multi-step "plan-execute-evaluate" loop rather than just a single turn. Let it read the output of the tool it just ran, decide if the problem is fixed, and if not, try a different approach.
Top comments (0)