Zurück zum Blog

AI Agent Programmierung mit Agno

AI Agent Programmierung mit Agno

Dieses Beispiel wird auf einem MacOS durchgeführt im Code Editor VS Code. Verwendet wird das Agno Framework.

Voraussetzungen für das Tutorial sind:

  • Lokale Python Installation
  • VS-Code oder ein anderer Code Editor

Python Package Manager installieren (uv)

uv installieren (macOS & Windows)

macOS (Homebrew)

Shell
brew install uv
uv --version

Windows (PowerShell)

Shell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
uv --version

Projektverzeichnis und uv-Projekt erstellen

Erstellen des Projektverzeichnisses

Shell
mkdir ai-agent-project

In das Projekt navigieren

Shell
cd ai-agent-project

Projekt initialisieren

Shell
uv init

Nun befinden sich im Projektverzeichnis folgende Dateien:

  • main.py
  • pyproject.toml
  • README.md

Das kann man kurz überprüfen indem man folgendes Kommando in das aktuelle Terminal eingibt:

Shell
ls

Erhalten wir die gewünschte Projektstruktur beginnen wir damit die notwendigen Pakete zu installieren:

Shell
uv add agno openai

Sofern python auf dem System vorhanden ist, wird die vorhandene Installation verwendet um ein Virtual Environment zu erstellen.

Im Projektverzeichnis sollten sich nun zwei Änderungen ergeben haben:

  • .venv (Neuer Ordner mit allen Abhängigkeiten und Paketen für das Projekt)
  • uv.lock (Datei, welche die Abhängigkeiten/ Pakete und Versionen fixiert)

Zusätzlich erstellen wir eine .env Datei, die all unsere Secret Keys wie den API Key für OpenAI oder Gemini beinhaltet.

Shell
nano .env

Dort erstellst du die Variable OPENAI_API_KEY:

OPENAI_API_KEY=<dein-key-kommt-hier-ohne-die-klammern-rein>

Nun haben wir alles um mit einem einfachen Agenten Beispiel zu beginnen. Hierfür öffnen wir VS-Code durch folgendes Kommando (oder regulär über die VS-Code Oberfläche in den Projektordner navigieren):

Öffnen von VS-Code

Shell
code .

Programmierung des Agenten

Statischer AI Agent ohne "Gedächtnis"

Für einen sehr simplem AI Agent, der bei der Ausführung immer den hartkodierten Prompt hat können wir folgenden Code in die main.py-Datei einfügen:

Python
# Imports
from dotenv import load_dotenv
from agno.agent import Agent
from agno.models.openai import OpenAIResponses

# Laden der Umgebungsvariablen
load_dotenv()

def main():
	# Agent mit OpenAI Modell erstellen
	agent = Agent(
		model=OpenAIResponses(id="gpt-4o-mini"),
		markdown=True,
	)
	# Anfrage an Agent senden und Antwort ausgeben
	agent.print_response("Tell me a very funny software engineering joke", stream=True)

if __name__ == "__main__":
	main()

Nach dem Ausführen des Codes ist das die Ausgabe auf der Konsole:

Message
> Tell me a very funny software engineering joke

Response (2.9s)
> Sure! Here's a classic:  
>  
> Why do programmers prefer dark mode?  
>  
> Because light attracts bugs! 🐛💻

Dynamischer KI Agent mit "Gedächtnis"

Unser bisheriger Agent hatte keine Möglichkeit sich den vorherigen Gesprächsverlauf zu "merken". Zudem gab es immer einen statischen Prompt, den wir im Code vor der Ausführung anpassen mussten.

Hierfür installieren wir uns folgendes Paket über uv:

Shell
uv add sqlalchemy

Um das zu verbessern nehmen wir hier eine temporäre Sqlite-Datenbank und starten den Agenten nun über eine CLI-App (CLI = Command Line Interface, also einer Kommunikation über die Kommandozeile).

Nach der ersten Ausführung des Codes ist im Projektverzeichnis der Ordner tmp mit der darin enthaltenen Datei agent.db. Wir fügen dem Agent-Objekt folgende Attribute hinzu:

  • db (Zeigt welche Art der Datenbank wir nutzen und wo sie liegt)
  • add_history_to_context (Sagt dem Agenten, dass der Gesprächsverlauf mitgenommen werden soll in jeden neuen Prompt)
  • num_history_runs (Maximale Anzahl der letzten Gesprächsrunden, die in den Kontext mit rein sollen)
Python
# Imports
from dotenv import load_dotenv
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses

# Laden der Umgebungsvariablen
load_dotenv()

def main():
	# Agent mit OpenAI Modell, Datenbank und Chat History erstellen
	agent = Agent(
		model=OpenAIResponses(id="gpt-4o-mini"),
		db=SqliteDb(db_file="tmp/agent.db"),
		add_history_to_context=True,
		num_history_runs=5,
		markdown=True,
		stream=True
	)

# Interaktive CLI starten
agent.cli_app(stream=True)
  
if __name__ == "__main__":
	main()

Nachdem man die kleine Anwendung startet kann man bereit solche Konversationen führen:

Markdown
Message
> Hi, ich bin Marios

Response (2.4s)
> Hallo Marios! Wie kann ich dir heute helfen?

Message
> Wie lautet mein Name?

Response (2.1s)
> Dein Name ist Marios. Wie kann ich dir weiterhelfen?

Message
> Erzähle mir einen lustigen Witz und verwende dabei meinen Namen

Response (1.7s)
> Klar, hier ist ein Witz mit deinem Namen:   
> Warum hat Marios einen GPS-Tracker in die Küche gelegt?  
> Weil er immer wieder in die "Maro-lokation" von Snacks verlorenging! 😂  
> Hoffe, das hat dir ein Lächeln ins Gesicht gezaubert!

Tools für den AI Agent

DuckDuckGo-Tool

Damit unser Agent mehr Fertigkeiten hat als ein einfacher Chatbot, können wir über Agno sogenannte Tools einbinden.

Hier gibt es die gesamte Liste an Tools: https://docs.agno.com/tools/toolkits/overview

Für unser nächstes Beispiel wählen wir das Tool für die Suchmaschine DuckDuckGo. Das installieren wir zunächst über uv:

Shell
uv add duckduckgo-search ddgs

Durch das Attribut tools übergeben wir dem Agenten die zur Verfügung stehenden Tools.

Python
# Imports
from dotenv import load_dotenv
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.tools.duckduckgo import DuckDuckGoTools

# Laden der Umgebungsvariablen
load_dotenv()

def main():
	# Agent mit OpenAI Modell, Datenbank, Chat History und Tools erstellen
    agent = Agent(
        model=OpenAIResponses(id="gpt-4o-mini"),
        db=SqliteDb(db_file="tmp/agent.db"),
        tools=[DuckDuckGoTools()],
        add_history_to_context=True,
        num_history_runs=5,
        markdown=True,
    )
    agent.cli_app(stream=True)

if __name__ == "__main__":
    main()

Nachfolgend sieht man wie nach Ausführung der App eine mögliche Konversation aussieht. Aus dem Verlauf kann man herauslesen, dass das KI Modell selbstständig entschieden hat das DuckDuckGo Tool zu nutzen, um die neusten Informationen zu bekommen.

Markdown
😎 User: Welche Neuigkeiten gibt es aktuell zu OpenAI? Fasse kurz zusammen in 3 Sätzen.

Message
> Welche Neuigkeiten gibt es aktuell zu OpenAI? Fasse kurz zusammen in 3 Sätzen.

Tool Calls
- search_news(query="OpenAI", max_results=5)

Response (8.7s)
Hier sind die neuesten Entwicklungen zu OpenAI:

1. OpenAI hat eine neue Version seines Codex-Tools eingeführt, die auf speziellen Chips basiert, was als erster Meilenstein in der Zusammenarbeit mit einem Chiphersteller gilt. [Artikel lesen](https://www.msn.com/en-us/news/technology/a-new-version-of-openai-s-codex-is-powered-by-a-new-dedicated-chip/ar-AA1WepWX).

2. Greg Brockman, der Präsident von OpenAI, hat Millionen an Donald Trump gespendet, um die Mission von OpenAI zu unterstützen, was bei einigen Mitarbeitern auf Unmut stößt. [Artikel lesen](https://www.wired.com/story/openai-president-greg-brockman-political-donations-trump-humanity/).

3. OpenAI hat die Cerebras-Chips für das neue Modell GPT-5.3 Codex Spark eingesetzt, um die Abhängigkeit von Nvidia zu verringern und die Generierungsgeschwindigkeit um das 15-fache zu steigern. [Artikel lesen](https://venturebeat.com/technology/openai-deploys-cerebras-chips-for-15x-faster-code-generation-in-first-major).

Arxiv-Tool

Ein weiteres interessantes Tool, das vor allem im wissenschaftlichen Kontext verwendet werden kann, ist das Arxiv-Tool.

Hierfür installieren wir uns, wie gehabt, über uv unsere Abhängigkeiten/ Pakete:

Shell
uv add arxiv pypdf

Für einen verbesserten Kontext setzen wir die Rolle unseres Agents mit dem Attribut instructions. Zusätzlich fügen wir dem Tools-Array das Arxiv-Tools hinzu.

Hier ist die gesamte Dokumentation des Tools zu finden: https://docs.agno.com/tools/toolkits/search/arxiv

Python
# Imports
from dotenv import load_dotenv
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.arxiv import ArxivTools

# Laden der Umgebungsvariablen
load_dotenv()

def main():
	# Agent mit OpenAI Modell, Datenbank, Chat History und Tools erstellen
	agent = Agent(
		model=OpenAIResponses(id="gpt-4o-mini"),
		db=SqliteDb(db_file="tmp/agent.db"),
		tools=[DuckDuckGoTools(), ArxivTools()],
		instructions=[
		"Du bist ein Forschungsassistent.",
		"Nutze ArxivTools um akademische Paper zu suchen und zusammenzufassen oder DuckDuckGo zum Suchen.",
		],
		add_history_to_context=True,
		num_history_runs=5,
		markdown=True,
	)
	agent.cli_app(stream=True)

if __name__ == "__main__":
	main()

So kann eine Konversation aussehen:

Markdown
😎 User: Suche nach Papers zum Thema LLMs und KI Agenten und fasse kurz zusammen.

Message
> Suche nach Papers zum Thema LLMs und KI Agenten und fasse kurz zusammen.

Tool Calls
- search_arxiv_and_return_articles(query="Large Language Models AI Agents", num_articles=5)
- search_arxiv_and_return_articles(query="LLMs AI Agents", num_articles=5)

Response (206.4s)
Hier sind einige aktuelle Papers zum Thema *Large Language Models (LLMs) und KI-Agenten*:

1. **Unmasking the Shadows of AI: Investigating Deceptive Capabilities in Large Language Models**  
   Autor: Linge Guo  
   Kurzfassung: Untersucht täuschendes Verhalten von LLMs und identifiziert vier Täuschungsarten (strategische Täuschung, Imitation, Schmeichelei, „untreues Denken“), inklusive sozialer Implikationen und möglicher Gegenmaßnahmen.  
   Links: http://arxiv.org/abs/2403.09676v1 · https://arxiv.org/pdf/2403.09676v1

2. **Is Self-knowledge and Action Consistent or Not: Investigating Large Language Model's Personality**  
   Autoren: Yiming Ai et al.  
   Kurzfassung: Prüft, ob klassische Persönlichkeitstests bei LLMs aussagekräftig sind, und ob „behauptete“ Persönlichkeitsmerkmale mit beobachtbarem Verhalten konsistent sind.  
   Links: http://arxiv.org/abs/2402.14679v2 · https://arxiv.org/pdf/2402.14679v2

3. **Enhancing Human-Like Responses in Large Language Models**  
   Autoren: Ethem Yağız Çalık, Talha Rüzgar Akkuş  
   Kurzfassung: Betrachtet Methoden, um LLM-Ausgaben menschlicher wirken zu lassen (u.a. natürlichere Sprache, emotionale Intelligenz) und nutzt Feinabstimmung zur Verbesserung der Mensch-KI-Interaktion.  
   Links: http://arxiv.org/abs/2501.05032v2 · https://arxiv.org/pdf/2501.05032v2

4. **Knowledge-Driven Agentic Scientific Corpus Distillation Framework for Biomedical Large Language Models Training**  
   Autoren: Meng Xiao et al.  
   Kurzfassung: Schlägt einen agentischen Multi-Agent-Ansatz vor, um hochwertige Trainingsdaten aus biomedizinischer Literatur zu destillieren und die Abhängigkeit von teuren/knappen Annotationen zu reduzieren.  
   Links: http://arxiv.org/abs/2504.19565v3 · https://arxiv.org/pdf/2504.19565v3

5. **Head-Specific Intervention Can Induce Misaligned AI Coordination in Large Language Models**  
   Autoren: Paul Darm, Annalisa Riccardi  
   Kurzfassung: Zeigt, dass gezielte Eingriffe zur Inferenzzeit (z.B. auf bestimmte Attention-Heads) Sicherheitsmechanismen beeinflussen/umgehen und unerwünschte Koordination bzw. Verhaltensänderungen auslösen können.  
   Links: http://arxiv.org/abs/2502.05945v3 · https://arxiv.org/pdf/2502.05945v3

Custom-Tools für den Agent

Oftmals reichen die fertigen Tools nicht aus für die spezifischen Business/ Anwendungsfälle. Zu den bereits vordefinierten Tools, die von Agno bereitstestellt werden, haben wir die Möglichkeit eigene Tools zu erstellen und dem Agenten zu übergeben. Dieser entscheidet dann selbstständig, ob er diese nutzt oder nicht.

In der Folge erstellen wir ein eigenes Tool, das speziell auf die HackerNews zugreift, um die neusten TechTrend für uns zu finden und zusammenzufassen.

Hierfür benötigen wir keine neuen Pakete, müssen allerdings json und httpx für die API-Calls und tool importieren. Für das Custom-Tool gibt es die spezielle Annotation @tool().

Weitere Informationen sind hier in der Dokumentation zu finden: https://docs.agno.com/cookbook/tools/custom-tools

Diese Funktion implementieren wir in unseren Code:

Python
# Imports
from dotenv import load_dotenv
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.arxiv import ArxivTools
import httpx
import json
from agno.tools import tool

# Laden der Umgebungsvariablen
load_dotenv()

@tool()  
def get_top_hackernews_stories(num_stories: int = 5) -> str:  
	"""Fetch top stories from Hacker News.  
	  
	Args:  
	num_stories (int): Number of stories to retrieve.  
	"""  
	response = httpx.get("https://hacker-news.firebaseio.com/v0/topstories.json")  
	story_ids = response.json()  
	  
	stories = []  
	for story_id in story_ids[:num_stories]:  
		story_response = httpx.get(  
		f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json"  
		)  
		story = story_response.json()  
		story.pop("text", None)  
		stories.append(story)  
  
return json.dumps(stories)

def main():
	# Agent mit OpenAI Modell, Datenbank, Chat History und Tools erstellen
	agent = Agent(
		model=OpenAIResponses(id="gpt-4o-mini"),
		db=SqliteDb(db_file="tmp/agent.db"),
		tools=[DuckDuckGoTools(), ArxivTools(), get_top_hackernews_stories],
		instructions=["Du bist ein Tech-News Assistent. Nutze das HackerNews Tool um aktuelle Stories zu finden."],
		add_history_to_context=True,
		num_history_runs=5,
		markdown=True,
	)
	agent.cli_app(stream=True)

if __name__ == "__main__":
	main()

Wir starten die App und chatten mit dem Agenten:

Markdown
😎 User: Welche Tech News gibt es? Fasse ganz kurz zusammen

Message
> Welche Tech News gibt es? Fasse ganz kurz zusammen

Tool Calls
- get_top_hackernews_stories(num_stories=5)

Response (11.9s)
Hier sind die aktuellen Tech-News von Hacker News:

1. **Zed Editor wechselt Grafiklibraries von blade zu wgpu**  
   Kurz: Größeres Rendering-Update, das Performance und Kompatibilität verbessern soll.  
   Link: https://github.com/zed-industries/zed/pull/46758

2. **Monosketch**  
   Kurz: Neue, simple Sketching-/Entwurfsplattform mit Fokus auf unkompliziertes Zeichnen.  
   Link: https://monosketch.io/

3. **Open Source Is Not About You (2018)**  
   Kurz: Essay/Reminder zur Philosophie und Haltung hinter Open Source.  
   Link: https://gist.github.com/richhickey/1563cddea1002958f96e7ba9519972d9

4. **Green’s Dictionary of Slang – 500 Jahre vulgäre Sprache**  
   Kurz: Umfangreiche Online-Referenz für Slang-Begriffe und deren Geschichte.  
   Link: https://greensdictofslang.com/

5. **Schneller als Dijkstra?**  
   Kurz: Diskussion über alternative Shortest-Path-Ansätze, die Dijkstra ggf. schlagen könnten.  
   Link: https://systemsapproach.org/2026/02/09/faster-than-dijkstra/

Agent über eine API verfügbar machen

Um den Agenten über eine Schnittstelle zu erreichen, nutzen wir von Agno das AgentOS Framework, das uns automatisch die Endpoints generiert. Für eine strukturierte Rückgabe nutzen wir zusätzlich FastAPI mit einem Response Schema.

Über die AgentOS-Dokumentation gibt es mehr Infos zum Vorgehen: https://docs.agno.com/agent-os/run-your-os

Zunächst installieren wir uns die Abgängigkeiten mit uv:

Shell
uv add "fastapi[standard]" sqlalchemy PyJWT

Dann nutzen wir folgenden Code:

Python
# Imports
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.arxiv import ArxivTools
from agno.os import AgentOS
from agno.tools import tool
import httpx
import json

# Laden der Umgebungsvariablen
load_dotenv()

@tool()
def get_top_hackernews_stories(num_stories: int = 5) -> str:
    """Fetch top stories from Hacker News.

    Args:
        num_stories (int): Number of stories to retrieve.
    """
    response = httpx.get("https://hacker-news.firebaseio.com/v0/topstories.json")
    story_ids = response.json()

    stories = []
    for story_id in story_ids[:num_stories]:
        story_response = httpx.get(
            f"https://hacker-news.firebaseio.com/v0/item/{story_id}.json"
        )
        story = story_response.json()
        story.pop("text", None)
        stories.append(story)

    return json.dumps(stories)

# Agent erstellen
agent = Agent(
    id="tech-agent",
    model=OpenAIResponses(id="gpt-4o-mini"),
    db=SqliteDb(db_file="tmp/agent.db"),
    tools=[DuckDuckGoTools(), ArxivTools(), get_top_hackernews_stories],
    instructions=["Du bist ein Tech-News Assistent. Nutze das HackerNews Tool um aktuelle Stories zu finden."],
    add_history_to_context=True,
    num_history_runs=5,
    markdown=True,
)

# Custom FastAPI App
custom_app = FastAPI(title="Tech Agent API")

# Request Schema
class ChatRequest(BaseModel):
    message: str
    session_id: str | None = None

# Sauberer Chat Endpoint
@custom_app.post("/chat")
def chat(request: ChatRequest):
    response = agent.run(request.message, session_id=request.session_id)
    return {
        "response": response.content,
        "session_id": response.session_id,
    }

# AgentOS mit Custom App als base_app
agent_os = AgentOS(
    agents=[agent],
    base_app=custom_app,
)

app = agent_os.get_app()

if __name__ == "__main__":
    agent_os.serve(app="main:app", reload=True)

Anschließend starten wir den Server mit:

Shell
python main.py

Der Agent ist unter folgenden URLs lokal verfügbar:

Shell
POST http://localhost:7777/agents/tech-agent/runs

Die automatisch generierte API Dokumentation ist aufrufbar unter:

Shell
http://localhost:7777/docs

Die Session lässt sich über diese URL verwalten:

Shell
GET http://localhost:7777/sessions

Zusätzlicher Endpoint /chat:

Shell
POST http://localhost:7777/chat

Durch AgentOS haben wir automatisch Zugriff auf:

  • Runs
  • Sessions
  • Memory
  • uvm.

Nun testen wir den Zugriff auf unseren Agent. Hierfür gibt es zwei Möglichkeiten:

  1. HTTP Client (zum Beispiel Postman)
  2. curl

Wir machen das nun am Beispiel von curl und senden folgende Anfrage:

Shell
curl -X POST http://localhost:7777/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Was sind die Top HackerNews Stories?"}'

Von unserer API erhalten wir folgende Antwort:

Json
{"response":"Hier sind die aktuellen Top-Stories von HackerNews:\n\n1. **[GPT-5.2 derives a new result in theoretical physics](https://openai.com/index/new-result-theoretical-physics/)**  \n   von davidbarker  \n   - Punkte: 28  \n   - Kommentare: 2  \n   - Zeit: 2023-12-12\n\n2. **[Apple, fix my keyboard before the timer ends or I'm leaving iPhone](https://ios-countdown.win/)**  \n   von ozzyphantom  \n   - Punkte: 783  \n   - Kommentare: 402  \n   - Zeit: 2023-12-12\n\n3. **[Monosketch](https://monosketch.io/)**  \n   von penguin_booze  \n   - Punkte: 539  \n   - Kommentare: 108  \n   - Zeit: 2023-12-12\n\n4. **[Sandwich Bill of Materials](https://nesbitt.io/2026/02/08/sandwich-bill-of-materials.html)**  \n   von zdw  \n   - Punkte: 92  \n   - Kommentare: 6  \n   - Zeit: 2023-12-12\n\n5. **[Open Source Is Not About You (2018)](https://gist.github.com/richhickey/1563cddea1002958f96e7ba9519972d9)**  \n   von doubleg  \n   - Punkte: 154  \n   - Kommentare: 100  \n   - Zeit: 2023-12-12\n\nFalls du mehr Informationen zu einer spezifischen Story möchtest, lass es mich wissen!",
"session_id":"b6212889-e8d1-450f-bc9d-5f4b344fd405"}

Agent Frontend

Damit wir die Daten nicht immer über die Konsole eingeben und auf Rohdaten in Form von JSON. Hierfür nutzen wir das leichtgewichtige und schnell eingerichtete Frontend von Streamlit.

In das bestehende Terminal geben wir folgende Kommandos ein:

Shell
uv add streamlit requests python-dotenv

Wir erstellen den Ordner frontend und darin die Datei app.py:

Shell
mkdir frontend
nano app.py

Dort fügen wir folgenden Code ein, der als Chat Interface dient:

Python
import streamlit as st
import requests
import json
import uuid

# Konfiguration
st.set_page_config(
	page_title="AI Agent Chat",
	page_icon="🤖",
	layout="wide"
)

st.title("🤖 AI Agent Chat")
st.markdown("---")

# Backend URL
API_URL = "http://localhost:7777"

# Initialize session state
if "messages" not in st.session_state:
	st.session_state.messages = []
	
if "session_id" not in st.session_state:
	st.session_state.session_id = str(uuid.uuid4())

# Zeige Chat Historie
for message in st.session_state.messages:
	with st.chat_message(message["role"]):
		st.markdown(message["content"])

  

# Chat input
if user_input := st.chat_input("Type your message..."):
	# Hinzufüger der User Nachricht zur Historie
	st.session_state.messages.append({
		"role": "user",
		"content": user_input
	})

	# Zeige Nachricht des Users
	with st.chat_message("user"):
		st.markdown(user_input)

	# Erhalte die Antwort vom Backend
	with st.chat_message("assistant"):
		try:
			response = requests.post(
				f"{API_URL}/chat",
				json={
					"message": user_input,
					"session_id": st.session_state.session_id
				},
			timeout=60
			)
			response.raise_for_status()
		
			result = response.json()
	
			# Zeige nur den Inhalt der Response
			response_text = result.get("response", "No response")
			st.markdown(response_text)
	
			# Füge zur Historie hinzu
			st.session_state.messages.append({
				"role": "assistant",
				"content": response_text
			})

		except requests.exceptions.ConnectionError:
			st.error(f"❌ Cannot connect to backend at {API_URL}")
		except requests.exceptions.Timeout:
			st.error("⏱️ Request timed out")
		except Exception as e:
		st.error(f"❌ Error: {str(e)}")

Nun müssen wir beide Anwendungen starten, wofür wir zwei Terminals benötigen.

Für unsere API:

Shell
python main.py

Für das Frontend:

Shell
cd frontend
streamlit run app.py

Multi Agent System

Nun haben wir einen Agenten inkl. Frontend der sehr spezialisiert ist auf TechNews. Wir hätten aber gerne ein Agente, der in der Lage ist selber zu entscheiden welche anderen spezialisierten Agenten aufgerufen werden. Also eine Art Orchestrator, der entscheidet wer am besten geeignet ist.

Hierfür bietet Agno die Möglichkeit von Teams an, wobei ein zentraler Agent als Koordinator fungiert.

Zunächst installieren wir uns eine weitere Abhängigkeit/ Paket über uv:

Shell
uv add yfinance

Anschließend ersetzen wir den Code der main.py durch folgenden:

Python
# Imports
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIResponses
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.arxiv import ArxivTools
from agno.tools.yfinance import YFinanceTools
from agno.tools.hackernews import HackerNewsTools
from agno.team import Team
from agno.os import AgentOS

# Laden der Umgebungsvariablen
load_dotenv()

db = SqliteDb(db_file="tmp/team.db")

# === Agenten erstellen ===

news_agent = Agent(
    name="News Agent",
    role="Du findest aktuelle Tech-News auf HackerNews.",
    model=OpenAIResponses(id="gpt-4o-mini"),
    tools=[HackerNewsTools()],
    instructions=["Finde aktuelle News und fasse sie zusammen."],
)

research_agent = Agent(
    name="Research Agent",
    role="Du recherchierst akademische Paper auf arXiv.",
    model=OpenAIResponses(id="gpt-4o-mini"),
    tools=[ArxivTools()],
    instructions=["Suche nach relevanten akademischen Papern und fasse die Ergebnisse zusammen."],
)

stock_agent = Agent(
    name="Stock Agent",
    role="Du analysierst Aktienkurse und Finanzdaten.",
    model=OpenAIResponses(id="gpt-4o-mini"),
    tools=[YFinanceTools()],
    instructions=["Liefere aktuelle Aktienkurse, Analysten-Empfehlungen und Finanzdaten."],
)

web_agent = Agent(
    name="Web Search Agent",
    role="Du durchsuchst das Web nach aktuellen Informationen.",
    model=OpenAIResponses(id="gpt-4o-mini"),
    tools=[DuckDuckGoTools()],
    instructions=["Suche im Web nach relevanten Informationen."],
)

# === Team erstellen ===

team = Team(
    id="research-team",
    name="Research Team",
    mode="coordinate",
    model=OpenAIResponses(id="gpt-4o-mini"),
    members=[news_agent, research_agent, stock_agent, web_agent],
    instructions=[
        "Du bist ein Koordinator, der Anfragen an die passenden Team-Mitglieder delegiert.",
        "Nutze den News Agent für aktuelle Tech-News.",
        "Nutze den Research Agent für akademische Recherchen.",
        "Nutze den Stock Agent für Aktien- und Finanzdaten.",
        "Nutze den Web Search Agent für allgemeine Websuchen.",
        "Fasse die Ergebnisse aller beteiligten Agenten zusammen.",
    ],
    db=db,
    add_history_to_context=True,
    num_history_runs=5,
    store_member_responses=True,
    share_member_interactions=True,
    markdown=True,
)

# === Custom FastAPI App ===

custom_app = FastAPI(title="Research Team API")

class ChatRequest(BaseModel):
    message: str
    session_id: str | None = None

class AgentInfo(BaseModel):
    name: str
    role: str
    content: str

class ChatResponse(BaseModel):
    response: str
    session_id: Optional[str]
    agents_used: List[AgentInfo]

@custom_app.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest):
    response = team.run(request.message, session_id=request.session_id)

    # Beteiligte Agenten aus member_responses extrahieren
    agents_used = []
    if response.member_responses:
        for member_response in response.member_responses:
            agent_name = getattr(member_response, "agent_id", None) or "Unknown"
            agents_used.append(AgentInfo(
                name=agent_name,
                role="Team Member",
                content=member_response.content or "",
            ))

    return ChatResponse(
        response=response.content,
        session_id=response.session_id,
        agents_used=agents_used,
    )

# === AgentOS mit Custom App ===

agent_os = AgentOS(
    agents=[news_agent, research_agent, stock_agent, web_agent],
    teams=[team],
    base_app=custom_app,
)

app = agent_os.get_app()

if __name__ == "__main__":
    agent_os.serve(app="main:app", reload=True)

Nun passen wir noch das Frontend entsprechend an:

Python
import streamlit as st
import requests
import json
import uuid
# Page config
st.set_page_config(
    page_title="AI Agent Chat",
    page_icon="🤖",
    layout="wide"
)
st.title("🤖 AI Agent Chat")
st.markdown("---")
# Backend URL
API_URL = "http://localhost:7777"
# Initialize session state
if "messages" not in st.session_state:
    st.session_state.messages = []
if "session_id" not in st.session_state:
    st.session_state.session_id = str(uuid.uuid4())
# Display chat history
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])
        # Show agents if available
        if "agents" in message and message["agents"]:
            with st.expander("👥 Agents involved"):
                for agent in message["agents"]:
                    st.markdown(f"**{agent['name']}** ({agent['role']})")
# Chat input
if user_input := st.chat_input("Type your message..."):
    # Add user message to history
    st.session_state.messages.append({
        "role": "user",
        "content": user_input,
        "agents": []
    })
    
    # Display user message
    with st.chat_message("user"):
        st.markdown(user_input)
    
    # Get response from backend
    with st.chat_message("assistant"):
        try:
            response = requests.post(
                f"{API_URL}/chat",
                json={
                    "message": user_input,
                    "session_id": st.session_state.session_id
                },
                timeout=60
            )
            response.raise_for_status()
            
            result = response.json()
            
            # Display the response content
            response_text = result.get("response", "No response")
            st.markdown(response_text)
            
            # Display agents used
            agents_used = result.get("agents_used", [])
            if agents_used:
                with st.expander("👥 Agents involved"):
                    for agent in agents_used:
                        st.markdown(f"**{agent['name']}** - {agent['role']}")
            
            # Add to history
            st.session_state.messages.append({
                "role": "assistant",
                "content": response_text,
                "agents": agents_used
            })
            
        except requests.exceptions.ConnectionError:
            st.error(f"❌ Cannot connect to backend at {API_URL}")
        except requests.exceptions.Timeout:
            st.error("⏱️ Request timed out")
        except Exception as e:
            st.error(f"❌ Error: {str(e)}")

Nun können wir wieder beide Anwendungen starten und entsprechende Prompts stellen. Nach jeder Antwort kommt ein Dropdown Menü, das anzeigt welche der vorhandenen Agenten verwendet wurde.

Implementieren von STT und TTS

Damit man mit den Agenten auch über natürliche Sprache reden und nicht nur schreiben kann, ist hier noch eine letzte Implementierung im Frontend.

Python
import streamlit as st
import requests
import json
import uuid
import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
# Load .env from parent directory
env_path = Path(__file__).parent.parent / ".env"
load_dotenv(env_path)
# Page config
st.set_page_config(
    page_title="AI Agent Chat",
    page_icon="🤖",
    layout="wide"
)
st.title("🤖 AI Agent Chat")
st.markdown("---")
# Backend URL
API_URL = "http://localhost:7777"
# OpenAI Client
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
# Initialize session state
if "messages" not in st.session_state:
    st.session_state.messages = []
if "session_id" not in st.session_state:
    st.session_state.session_id = str(uuid.uuid4())
if "input_mode" not in st.session_state:
    st.session_state.input_mode = "text"
if "last_audio_played" not in st.session_state:
    st.session_state.last_audio_played = None
# Display chat history (WITHOUT audio - audio will be shown separately)
for i, message in enumerate(st.session_state.messages):
    with st.chat_message(message["role"]):
        st.markdown(message["content"])
        # Show agents if available
        if "agents" in message and message["agents"]:
            with st.expander("👥 Agents involved"):
                for agent in message["agents"]:
                    st.markdown(f"**{agent['name']}** ({agent['role']})")
# Input mode toggle
st.markdown("### 📝 Input Mode")
col1, col2 = st.columns(2)
with col1:
    if st.button("⌨️ Text Input", use_container_width=True, 
                 type="primary" if st.session_state.input_mode == "text" else "secondary"):
        st.session_state.input_mode = "text"
        st.rerun()
with col2:
    if st.button("🎤 Voice Input", use_container_width=True,
                 type="primary" if st.session_state.input_mode == "voice" else "secondary"):
        st.session_state.input_mode = "voice"
        st.rerun()
st.markdown("---")
# Display selected input method
user_input = None
if st.session_state.input_mode == "text":
    text_input = st.chat_input("Type your message...")
    user_input = text_input
else:  # voice mode
    audio_input = st.audio_input("Record your message", key="audio_input")
    
    if audio_input:
        try:
            # Convert audio to text with Whisper
            with st.spinner("🎯 Transcribing..."):
                transcript = client.audio.transcriptions.create(
                    model="whisper-1",
                    file=("audio.wav", audio_input, "audio/wav")
                )
                user_input = transcript.text
                st.success(f"Transcribed: {user_input}")
        except Exception as e:
            st.error(f"❌ Transcription failed: {type(e).__name__}: {str(e)}")
            user_input = None
# Process input
if user_input:
    # Add user message to history
    st.session_state.messages.append({
        "role": "user",
        "content": user_input,
        "agents": []
    })
    
    # Display user message
    with st.chat_message("user"):
        st.markdown(user_input)
    
    # Get response from backend
    with st.chat_message("assistant"):
        try:
            response = requests.post(
                f"{API_URL}/chat",
                json={
                    "message": user_input,
                    "session_id": st.session_state.session_id
                },
                timeout=180
            )
            response.raise_for_status()
            result = response.json()
            
            # Display the response content
            response_text = result.get("response", "No response")
            st.markdown(response_text)
            
            # Display agents used
            agents_used = result.get("agents_used", [])
            if agents_used:
                with st.expander("👥 Agents involved"):
                    for agent in agents_used:
                        st.markdown(f"**{agent['name']}** - {agent['role']}")
            
            # Convert response to speech
            with st.spinner("🔊 Generating audio..."):
                audio_response = client.audio.speech.create(
                    model="gpt-4o-mini-tts-2025-12-15",
                    voice="echo",
                    input=response_text
                )
                audio_bytes = audio_response.content
                st.audio(audio_bytes, format="audio/mpeg", autoplay=True)
            
            # Add to history
            st.session_state.messages.append({
                "role": "assistant",
                "content": response_text,
                "agents": agents_used
            })
            
        except requests.exceptions.ConnectionError:
            st.error(f"❌ Cannot connect to backend at {API_URL}")
        except requests.exceptions.Timeout:
            st.error("⏱️ Request timed out")
        except Exception as e:
            st.error(f"❌ Error: {type(e).__name__}: {str(e)}")
# Show continue input if chat exists
if st.session_state.messages:
    st.markdown("---")
    st.write("💬 Continue the conversation:")
    
    if st.session_state.input_mode == "text":
        continue_input = st.chat_input("Type your next message...")
    else:  # voice mode
        continue_input = None
        continue_audio = st.audio_input("Record your next message", key="continue_audio")
        if continue_audio:
            try:
                with st.spinner("🎯 Transcribing..."):
                    transcript = client.audio.transcriptions.create(
                        model="whisper-1",
                        file=("audio.wav", continue_audio, "audio/wav")
                    )
                    continue_input = transcript.text
            except Exception as e:
                st.error(f"❌ Transcription failed: {str(e)}")
    
    # Process continue input
    if continue_input:
        # Add user message
        st.session_state.messages.append({
            "role": "user",
            "content": continue_input,
            "agents": []
        })
        
        # Display user message
        with st.chat_message("user"):
            st.markdown(continue_input)
        
        # Get response
        with st.chat_message("assistant"):
            try:
                response = requests.post(
                    f"{API_URL}/chat",
                    json={
                        "message": continue_input,
                        "session_id": st.session_state.session_id
                    },
                    timeout=180
                )
                response.raise_for_status()
                result = response.json()
                
                # Display response
                response_text = result.get("response", "No response")
                st.markdown(response_text)
                
                # Display agents
                agents_used = result.get("agents_used", [])
                if agents_used:
                    with st.expander("👥 Agents involved"):
                        for agent in agents_used:
                            st.markdown(f"**{agent['name']}** - {agent['role']}")
                
                # Generate and play audio
                with st.spinner("🔊 Generating audio..."):
                    audio_response = client.audio.speech.create(
                        model="gpt-4o-mini-tts-2025-12-15",
                        voice="echo",
                        input=response_text
                    )
                    audio_bytes = audio_response.content
                    st.audio(audio_bytes, format="audio/mpeg", autoplay=True)
                
                # Add to history
                st.session_state.messages.append({
                    "role": "assistant",
                    "content": response_text,
                    "agents": agents_used
                })
                
            except Exception as e:
                st.error(f"❌ Error: {type(e).__name__}: {str(e)}")

Ich hoffe, dass jeder der den Beitrag gelesen hat oder sogar mitprogrammiert hat spaß dran hatte und vor allem etwas neues dazu gelernt hat. AI Agents sind noch ein relativ neues Feld, was uns in Zukunft aber immer weiter begleiten wird, weshalb es wichtig ist am Ball zu bleiben und sich immer wieder neues wissen anzueignen.

Für diejenigen, die nicht Schritt für Schritt mitprogrammieren wollen/ können, hier das gesamte Repository: https://github.com/DFT-IT/agno-ai-agent-project.git

Bis zum nächsten mal! :) -- Marios Tzialidis