Kerntypen
Grundlegende Typen und Traits aus adk-core, die das Fundament von ADK-Rust bilden.
Content und Part
Jede Nachricht in ADK fließt durch Content und Part. Das Verständnis dieser Typen ist essenziell für die Arbeit mit Agenten, Tools und Callbacks.
Content
Content repräsentiert eine einzelne Nachricht in einer Konversation. Es hat eine role (wer sie gesendet hat) und einen oder mehrere parts (den eigentlichen Inhalt).
Rollen:
"user"- Nachrichten vom Benutzer"model"- Antworten vom AI model"tool"- Ergebnisse der Tool-Ausführung
use adk_core::Content;
// Simple text message from user
let user_msg = Content::new("user")
.with_text("What's the weather like?");
// Model response
let model_msg = Content::new("model")
.with_text("The weather is sunny and 72°F.");
// Multiple text parts in one message
let detailed_msg = Content::new("user")
.with_text("Here's my question:")
.with_text("What is the capital of France?");
Multimodaler Content:
Content kann Bilder, Audio, PDFs und andere Binärdaten neben Text enthalten:
// Image from bytes (e.g., read from file)
let image_bytes = std::fs::read("photo.jpg")?;
let content = Content::new("user")
.with_text("What's in this image?")
.with_inline_data("image/jpeg", image_bytes);
// Image from URL (model fetches it)
let content = Content::new("user")
.with_text("Describe this image")
.with_file_uri("image/png", "https://example.com/chart.png");
// PDF document
let pdf_bytes = std::fs::read("report.pdf")?;
let content = Content::new("user")
.with_text("Summarize this document")
.with_inline_data("application/pdf", pdf_bytes);
Part
Part ist ein enum, das verschiedene Arten von Inhalt innerhalb einer Nachricht repräsentiert:
pub enum Part {
// Klartext
Text { text: String },
// Binärdaten, die in die Nachricht eingebettet sind
InlineData { mime_type: String, data: Vec<u8> },
// Referenz auf externe Datei (URL oder Cloud-Speicher)
FileData { mime_type: String, file_uri: String },
// Model fordert einen Tool-Aufruf an
FunctionCall { name: String, args: Value, id: Option<String> },
// Ergebnis einer Tool-Ausführung
FunctionResponse { function_response: FunctionResponseData, id: Option<String> },
}
Parts direkt erstellen:
use adk_core::Part;
// Text-Part
let text = Part::text_part("Hello, world!");
// Bild aus Bytes
let image = Part::inline_data("image/png", png_bytes);
// Bild von URL
let remote_image = Part::file_data("image/jpeg", "https://example.com/photo.jpg");
Parts inspizieren:
// Textinhalt abrufen (gibt None für Nicht-Text-Parts zurück)
if let Some(text) = part.text() {
println!("Text: {}", text);
}
// MIME-Typ abrufen (für InlineData und FileData)
if let Some(mime) = part.mime_type() {
println!("MIME type: {}", mime);
}
// file URI abrufen (nur für FileData)
if let Some(uri) = part.file_uri() {
println!("File URI: {}", uri);
}
// Prüfen, ob der Part Medien enthält (Bild, Audio, Video, etc.)
if part.is_media() {
println!("This part contains binary media");
}
Über Parts iterieren:
for part in &content.parts {
match part {
Part::Text { text } => println!("Text: {}", text),
Part::InlineData { mime_type, data } => {
println!("Binary data: {} ({} bytes)", mime_type, data.len());
}
Part::FileData { mime_type, file_uri } => {
println!("File: {} at {}", mime_type, file_uri);
}
Part::FunctionCall { name, args, .. } => {
println!("Tool call: {}({})", name, args);
}
Part::FunctionResponse { function_response, .. } => {
println!("Tool result: {} -> {}",
function_response.name,
function_response.response
);
}
}
}
Agent Trait
Das Agent trait ist die zentrale Abstraktion für alle Agents im ADK. Jeder Agent-Typ – LLM Agents, Workflow Agents, benutzerdefinierte Agents – implementiert dieses trait.
#[async_trait]
pub trait Agent: Send + Sync {
/// Unique identifier for this agent
fn name(&self) -> &str;
/// Human-readable description of what this agent does
fn description(&self) -> &str;
/// Child agents (for workflow agents like Sequential, Parallel)
fn sub_agents(&self) -> &[Arc<dyn Agent>];
/// Execute the agent and return a stream of events
async fn run(&self, ctx: Arc<dyn InvocationContext>) -> Result<EventStream>;
}
Wichtige Punkte:
name(): Wird für Logging, Transfers und Identifikation verwendet. Muss innerhalb eines Multi-Agenten-Systems eindeutig sein.description(): Wird LLMs gezeigt, wenn der Agent als Tool oder für Routing-Entscheidungen verwendet wird.sub_agents(): Gibt untergeordnete Agents zurück. Leer für Leaf Agents (LlmAgent), befüllt für Container (SequentialAgent, ParallelAgent).run(): Die Hauptausführungsmethode. Empfängt Kontext und gibt einen EventStream zurück.
Warum EventStream?
Agents geben einen EventStream (einen Stream von Result<Event>) zurück, anstatt einer einzelnen Antwort, weil:
- Streaming: Antworten können Token für Token gestreamt werden für eine bessere UX.
- Tool calls: Mehrere Tool calls und Antworten erfolgen während der Ausführung.
- Zustandsänderungen: Zustandsaktualisierungen werden als Events ausgegeben.
- Transfers: Agent transfers werden durch Events signalisiert.
Tool Trait
Tools erweitern die Fähigkeiten von Agenten über die Konversation hinaus. Sie ermöglichen es Agenten, mit APIs, Datenbanken, Dateisystemen zu interagieren oder Berechnungen durchzuführen.
#[async_trait]
pub trait Tool: Send + Sync {
/// Tool name (used in function calls from the model)
fn name(&self) -> &str;
/// Description shown to the LLM to help it decide when to use this tool
fn description(&self) -> &str;
/// JSON Schema defining the expected parameters
fn parameters_schema(&self) -> Option<Value> { None }
/// JSON Schema defining the response format
fn response_schema(&self) -> Option<Value> { None }
/// Whether this tool runs asynchronously (returns task ID immediately)
fn is_long_running(&self) -> bool { false }
/// Execute the tool with given arguments
async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value>;
}
Wichtige Punkte:
name(): Der Funktionsname, den das Modell verwendet, um dieses Tool aufzurufen. Halten Sie ihn kurz und deskriptiv (z.B.get_weather,search_database).description(): Entscheidend, damit das Modell versteht, wann das Tool verwendet werden soll. Seien Sie spezifisch bezüglich dessen, was es tut und wann es verwendet werden soll.parameters_schema(): JSON Schema, das dem Modell mitteilt, welche Argumente bereitzustellen sind. Ohne dies rät das Modell.execute(): Empfängt geparste Argumente alsserde_json::Value. Geben Sie das Ergebnis als JSON zurück.
Implementierung eines benutzerdefinierten Tools:
use adk_core::{Tool, ToolContext, Result};
use async_trait::async_trait;
use serde_json::{json, Value};
use std::sync::Arc;
struct WeatherTool {
api_key: String,
}
#[async_trait]
impl Tool for WeatherTool {
fn name(&self) -> &str {
"get_weather"
}
fn description(&self) -> &str {
"Get current weather for a city. Use this when the user asks about weather conditions."
}
fn parameters_schema(&self) -> Option<Value> {
Some(json!({
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name (e.g., 'London', 'New York')"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature units"
}
},
"required": ["city"]
}))
}
async fn execute(&self, _ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
let city = args["city"].as_str().unwrap_or("Unknown");
let units = args["units"].as_str().unwrap_or("celsius");
// Call weather API here...
Ok(json!({
"city": city,
"temperature": 22,
"units": units,
"condition": "sunny"
}))
}
}
Langlaufende Tools:
Für Operationen, die lange dauern (Dateiverarbeitung, externe API-Aufrufe), kennzeichnen Sie das Tool als langlaufend:
fn is_long_running(&self) -> bool { true }
Langlaufende Tools geben sofort eine Aufgaben-ID zurück. Dem Modell wird mitgeteilt, das Tool nicht erneut aufzurufen, solange es ausstehend ist.
Toolset Trait
Toolset stellt Tools dynamisch basierend auf dem Kontext bereit. Verwenden Sie dies, wenn:
- Tools von Benutzerberechtigungen abhängen
- Tools von externen Quellen (MCP servers) geladen werden
- die Verfügbarkeit von Tools während der Ausführung variiert
#[async_trait]
pub trait Toolset: Send + Sync {
/// Toolset-Bezeichner
fn name(&self) -> &str;
/// Gibt verfügbare Tools für den aktuellen Kontext zurück
async fn tools(&self, ctx: Arc<dyn ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>>;
}
Beispiel: Berechtigungsbasiertes Toolset:
struct AdminToolset {
admin_tools: Vec<Arc<dyn Tool>>,
user_tools: Vec<Arc<dyn Tool>>,
}
#[async_trait]
impl Toolset for AdminToolset {
fn name(&self) -> &str { "admin_toolset" }
async fn tools(&self, ctx: Arc<dyn ReadonlyContext>) -> Result<Vec<Arc<dyn Tool>>> {
if ctx.user_id().starts_with("admin_") {
Ok(self.admin_tools.clone())
} else {
Ok(self.user_tools.clone())
}
}
}
Kontext-Traits
Kontexte stellen Informationen und Dienste für Agents und Tools während der Ausführung bereit. Es gibt eine Hierarchie von Kontext-Traits, wobei jeder weitere Fähigkeiten hinzufügt.
ReadonlyContext
Grundlegende Informationen, die überall verfügbar sind:
pub trait ReadonlyContext: Send + Sync {
/// Eindeutige ID für diese Invocation
fn invocation_id(&self) -> &str;
/// Name des aktuell ausführenden Agent
fn agent_name(&self) -> &str;
/// Benutzer-Bezeichner
fn user_id(&self) -> &str;
/// Anwendungsname
fn app_name(&self) -> &str;
/// Session-Bezeichner
fn session_id(&self) -> &str;
/// Die Benutzereingabe, die diese Invocation ausgelöst hat
fn user_content(&self) -> &Content;
}
CallbackContext
Fügt Zugriff auf Artefakte hinzu (erweitert ReadonlyContext):
pub trait CallbackContext: ReadonlyContext {
/// Zugriff auf Artefakt-Speicher (falls konfiguriert)
fn artifacts(&self) -> Option<Arc<dyn Artifacts>>;
}
ToolContext
Für die Tool-Ausführung (erweitert CallbackContext):
pub trait ToolContext: CallbackContext {
/// ID des Funktionsaufrufs, der dieses Tool ausgelöst hat
fn function_call_id(&self) -> &str;
/// Holt aktuelle Ereignisaktionen (transfer, escalate, etc.)
fn actions(&self) -> EventActions;
/// Setzt Ereignisaktionen (z.B. Auslösen eines transfers)
fn set_actions(&self, actions: EventActions);
/// Langzeit-Memory durchsuchen
async fn search_memory(&self, query: &str) -> Result<Vec<MemoryEntry>>;
}
InvocationContext
Vollständiger Kontext für die Agent-Ausführung (erweitert CallbackContext):
pub trait InvocationContext: CallbackContext {
/// Der aktuell ausgeführte Agent
fn agent(&self) -> Arc<dyn Agent>;
/// Memory-Dienst (falls konfiguriert)
fn memory(&self) -> Option<Arc<dyn Memory>>;
/// Aktuelle Session mit Status und Historie
fn session(&self) -> &dyn Session;
/// Ausführungskonfiguration
fn run_config(&self) -> &RunConfig;
/// Signalisiert, dass diese Invocation beendet werden soll
fn end_invocation(&self);
/// Prüft, ob die Invocation beendet wurde
fn ended(&self) -> bool;
}
Sitzung und Zustand
Sitzungen verfolgen Konversationen. Zustand speichert Daten innerhalb von Sitzungen.
Sitzung
pub trait Session: Send + Sync {
/// Eindeutiger Sitzungsbezeichner
fn id(&self) -> &str;
/// Anwendung, zu der diese Sitzung gehört
fn app_name(&self) -> &str;
/// Benutzer, dem diese Sitzung gehört
fn user_id(&self) -> &str;
/// Veränderlicher Zustandspeicher
fn state(&self) -> &dyn State;
/// Vorherige Nachrichten in dieser Konversation
fn conversation_history(&self) -> Vec<Content>;
}
Zustand
Schlüssel-Wert-Speicher mit bereichsbezogenen Präfixen:
pub trait State: Send + Sync {
/// Wert nach Schlüssel abrufen
fn get(&self, key: &str) -> Option<Value>;
/// Wert setzen
fn set(&mut self, key: String, value: Value);
/// Alle Schlüssel-Wert-Paare abrufen
fn all(&self) -> HashMap<String, Value>;
}
Zustandspräfixe:
Zustandsschlüssel verwenden Präfixe, um den Bereich und die Persistenz zu steuern:
| Präfix | Bereich | Persistenz | Anwendungsfall |
|---|---|---|---|
user: | Benutzer-Ebene | Sitzungsübergreifend | Benutzereinstellungen, Präferenzen |
app: | Anwendungs-Ebene | Anwendungsweit | Gemeinsame Konfiguration |
temp: | Zug-Ebene | Wird bei jedem Zug gelöscht | Temporäre Berechnungsdaten |
| (none) | Sitzungs-Ebene | Nur diese Sitzung | Konversationskontext |
// In einem Callback oder Tool
let state = ctx.session().state();
// Benutzereinstellung (bleibt sitzungsübergreifend bestehen)
state.set("user:theme".into(), json!("dark"));
// Sitzungsspezifische Daten
state.set("current_topic".into(), json!("weather"));
// Temporäre Daten (nach diesem Zug gelöscht)
state.set("temp:step_count".into(), json!(1));
// Werte lesen
if let Some(theme) = state.get("user:theme") {
println!("Theme: {}", theme);
}
Fehlerbehandlung
ADK verwendet einen vereinheitlichten Fehlertyp für alle Operationen:
pub enum AdkError {
Agent(String), // Fehler bei der Agenten-Ausführung
Tool(String), // Fehler bei der Tool-Ausführung
Model(String), // LLM API-Fehler
Session(String), // Fehler bei der Sitzungsspeicherung
Artifact(String), // Fehler bei der Artefaktspeicherung
Config(String), // Konfigurationsfehler
Io(std::io::Error), // Datei-/Netzwerk-I/O-Fehler
Json(serde_json::Error), // JSON-Analysefehler
}
pub type Result<T> = std::result::Result<T, AdkError>;
Fehlerbehandlung in Tools:
async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
let city = args["city"]
.as_str()
.ok_or_else(|| AdkError::Tool("Missing 'city' parameter".into()))?;
let response = reqwest::get(&format!("https://api.weather.com/{}", city))
.await
.map_err(|e| AdkError::Tool(format!("API error: {}", e)))?;
Ok(json!({ "weather": "sunny" }))
}
Ereignis-Stream
Agents geben einen Stream von Ereignissen zurück, anstatt einer einzelnen Antwort:
pub type EventStream = Pin<Box<dyn Stream<Item = Result<Event>> + Send>>;
Ereignisse verarbeiten:
use futures::StreamExt;
let mut stream = agent.run(ctx).await?;
while let Some(result) = stream.next().await {
match result {
Ok(event) => {
// Nach Textinhalt suchen
if let Some(content) = event.content() {
for part in &content.parts {
if let Some(text) = part.text() {
print!("{}", text);
}
}
}
// Nach Zustandsänderungen suchen
for (key, value) in event.state_delta() {
println!("State changed: {} = {}", key, value);
}
// Prüfen, ob dies die endgültige Antwort ist
if event.is_final_response() {
println!("\n[Done]");
}
}
Err(e) => {
eprintln!("Error: {}", e);
break;
}
}
}
Llm Trait
Das Trait, das alle LLM-Anbieter implementieren:
#[async_trait]
pub trait Llm: Send + Sync {
/// Modell-Identifikator (z.B. "gemini-2.0-flash", "gpt-4o")
fn name(&self) -> &str;
/// Inhalt generieren (Streaming oder Nicht-Streaming)
async fn generate_content(
&self,
request: LlmRequest,
stream: bool,
) -> Result<LlmResponseStream>;
}
LlmRequest:
pub struct LlmRequest {
pub contents: Vec<Content>, // Konversationsverlauf
pub tools: Vec<ToolDeclaration>, // Verfügbare Tools
pub system_instruction: Option<String>, // System-Prompt
pub config: GenerateContentConfig, // Temperatur, max_tokens, etc.
}
LlmResponse:
pub struct LlmResponse {
pub content: Option<Content>, // Generierter Inhalt
pub finish_reason: Option<FinishReason>, // Warum die Generierung gestoppt wurde
pub usage: Option<UsageMetadata>, // Token-Zählungen
pub partial: bool, // Ist dies ein Streaming-Chunk?
pub turn_complete: bool, // Ist der Turn beendet?
}
Alle Anbieter (Gemini, OpenAI, Anthropic, Ollama, etc.) implementieren dieses Trait, wodurch sie austauschbar sind:
// Anbieter durch Ändern einer Zeile wechseln
let model: Arc<dyn Llm> = Arc::new(GeminiModel::new(&key, "gemini-2.0-flash")?);
// let model: Arc<dyn Llm> = Arc::new(OpenAIClient::new(config)?);
// let model: Arc<dyn Llm> = Arc::new(AnthropicClient::new(config)?);
let agent = LlmAgentBuilder::new("assistant")
.model(model)
.build()?;
Zurück: ← Einführung | Weiter: Runner →