e071084ebe
govulncheck / govulncheck (push) Waiting to run
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
247 lines
7.3 KiB
Go
247 lines
7.3 KiB
Go
// Agent Ollama — a self-contained agent powered by Ollama Cloud.
|
|
//
|
|
// This example demonstrates the full harness loop — service tools, custom
|
|
// tools, agent memory, guardrails, and streaming — using the Ollama
|
|
// provider with gpt-oss:120b on Ollama Cloud.
|
|
//
|
|
// It creates a "knowledge" service with two endpoints (Add, Search) that
|
|
// the agent discovers as tools, plus a custom "current_time" tool. The
|
|
// agent answers natural-language questions by calling those tools.
|
|
//
|
|
// Run (Ollama Cloud — default):
|
|
//
|
|
// OLLAMA_API_KEY=your-key go run main.go
|
|
//
|
|
// Run (local Ollama):
|
|
//
|
|
// OLLAMA_BASE_URL=http://localhost:11434 \
|
|
// OLLAMA_MODEL=llama3.2 \
|
|
// go run main.go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go-micro.dev/v6"
|
|
"go-micro.dev/v6/agent"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// knowledge service — a tiny in-memory knowledge base
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type KnowledgeEntry struct {
|
|
ID string `json:"id" description:"Unique entry identifier"`
|
|
Topic string `json:"topic" description:"Topic or category"`
|
|
Content string `json:"content" description:"The knowledge content"`
|
|
}
|
|
|
|
type AddKnowledgeRequest struct {
|
|
Topic string `json:"topic" description:"Topic or category (required)"`
|
|
Content string `json:"content" description:"The knowledge content (required)"`
|
|
}
|
|
|
|
type AddKnowledgeResponse struct {
|
|
Entry *KnowledgeEntry `json:"entry" description:"The added entry"`
|
|
}
|
|
|
|
type SearchKnowledgeRequest struct {
|
|
Topic string `json:"topic,omitempty" description:"Filter by topic (optional)"`
|
|
Keyword string `json:"keyword,omitempty" description:"Search keyword in content (optional)"`
|
|
}
|
|
|
|
type SearchKnowledgeResponse struct {
|
|
Entries []*KnowledgeEntry `json:"entries" description:"Matching entries"`
|
|
}
|
|
|
|
type KnowledgeService struct {
|
|
mu sync.RWMutex
|
|
entries []*KnowledgeEntry
|
|
nextID int
|
|
}
|
|
|
|
// Add stores a new knowledge entry.
|
|
//
|
|
// @example {"topic": "go", "content": "Go interfaces are implicit."}
|
|
func (s *KnowledgeService) Add(ctx context.Context, req *AddKnowledgeRequest, rsp *AddKnowledgeResponse) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.nextID++
|
|
e := &KnowledgeEntry{
|
|
ID: fmt.Sprintf("kb-%d", s.nextID),
|
|
Topic: req.Topic,
|
|
Content: req.Content,
|
|
}
|
|
s.entries = append(s.entries, e)
|
|
rsp.Entry = e
|
|
return nil
|
|
}
|
|
|
|
// Search finds knowledge entries by topic or keyword.
|
|
//
|
|
// @example {"topic": "go"}
|
|
// @example {"keyword": "interface"}
|
|
func (s *KnowledgeService) Search(ctx context.Context, req *SearchKnowledgeRequest, rsp *SearchKnowledgeResponse) error {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
for _, e := range s.entries {
|
|
if req.Topic != "" && !strings.EqualFold(e.Topic, req.Topic) {
|
|
continue
|
|
}
|
|
if req.Keyword != "" && !strings.Contains(strings.ToLower(e.Content), strings.ToLower(req.Keyword)) {
|
|
continue
|
|
}
|
|
rsp.Entries = append(rsp.Entries, e)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// main
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func main() {
|
|
// Ollama Cloud is the default. Override with env vars for local Ollama.
|
|
baseURL := os.Getenv("OLLAMA_BASE_URL")
|
|
if baseURL == "" {
|
|
baseURL = "https://ollama.com/v1"
|
|
}
|
|
model := os.Getenv("OLLAMA_MODEL")
|
|
if model == "" {
|
|
model = "gpt-oss:120b"
|
|
}
|
|
apiKey := os.Getenv("OLLAMA_API_KEY")
|
|
|
|
fmt.Println("╔══════════════════════════════════════════╗")
|
|
fmt.Println("║ Ollama-Powered Go Micro Agent ║")
|
|
fmt.Println("╚══════════════════════════════════════════╝")
|
|
fmt.Println()
|
|
fmt.Printf(" Ollama URL: %s\n", baseURL)
|
|
fmt.Printf(" Model: %s\n", model)
|
|
if apiKey != "" {
|
|
fmt.Printf(" API Key: (set)\n")
|
|
} else {
|
|
fmt.Printf(" API Key: (none — set OLLAMA_API_KEY)\n")
|
|
}
|
|
fmt.Println()
|
|
|
|
// 1. Start the knowledge service. Its handlers become agent tools.
|
|
svc := micro.NewService("knowledge")
|
|
svc.Handle(new(KnowledgeService))
|
|
go svc.Run()
|
|
|
|
// Give the service a moment to register.
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// 2. Create the agent. It discovers the knowledge service endpoints
|
|
// as tools automatically, plus gets a custom "current_time" tool.
|
|
ag := micro.NewAgent("ollama-assistant",
|
|
micro.AgentServices("knowledge"),
|
|
micro.AgentPrompt(
|
|
"You are a helpful knowledge assistant. You can search and add to "+
|
|
"a knowledge base using the knowledge service tools. "+
|
|
"When asked about the current time, use the current_time tool. "+
|
|
"Be concise and factual.",
|
|
),
|
|
micro.AgentProvider("ollama"),
|
|
micro.AgentModel(model),
|
|
micro.AgentAPIKey(apiKey),
|
|
micro.AgentBaseURL(baseURL),
|
|
micro.AgentMaxSteps(10),
|
|
micro.AgentLoopLimit(3),
|
|
// Custom tool — any function, not tied to a service.
|
|
agent.WithTool(
|
|
"current_time",
|
|
"Get the current date and time in a human-readable format",
|
|
map[string]any{
|
|
"timezone": map[string]any{
|
|
"type": "string",
|
|
"description": "Optional timezone (defaults to local)",
|
|
},
|
|
},
|
|
func(ctx context.Context, input map[string]any) (string, error) {
|
|
tz, _ := input["timezone"].(string)
|
|
if tz == "" {
|
|
return time.Now().Format("2006-01-02 15:04:05 MST"), nil
|
|
}
|
|
loc, err := time.LoadLocation(tz)
|
|
if err != nil {
|
|
return "", fmt.Errorf("unknown timezone: %s", tz)
|
|
}
|
|
return time.Now().In(loc).Format("2006-01-02 15:04:05 MST"), nil
|
|
},
|
|
),
|
|
)
|
|
|
|
// 3. Seed initial knowledge via the agent's first question.
|
|
questions := []string{
|
|
"What time is it now?",
|
|
"Add a new knowledge entry: topic 'go', content 'Go interfaces are implicit — a type implements an interface by having the required methods.'",
|
|
"Add another entry: topic 'go', content 'Go is a statically typed, compiled language designed at Google.'",
|
|
"Add another entry: topic 'ai', content 'Large language models generate text by predicting the next token in a sequence.'",
|
|
"Search the knowledge base for entries about Go.",
|
|
"Search for everything in the knowledge base.",
|
|
}
|
|
|
|
fmt.Println("─── Agent Demo ───")
|
|
fmt.Println()
|
|
|
|
for i, q := range questions {
|
|
fmt.Printf("Q%d: %s\n", i+1, q)
|
|
fmt.Print("A: ")
|
|
|
|
resp, err := ag.Ask(context.Background(), q)
|
|
if err != nil {
|
|
fmt.Printf("error: %v\n", err)
|
|
fmt.Println()
|
|
continue
|
|
}
|
|
|
|
// Show tool calls the agent made.
|
|
if len(resp.ToolCalls) > 0 {
|
|
for _, tc := range resp.ToolCalls {
|
|
args, _ := json.Marshal(tc.Input)
|
|
fmt.Printf(" [tool] %s(%s)\n", tc.Name, string(args))
|
|
}
|
|
}
|
|
|
|
fmt.Println(resp.Reply)
|
|
if resp.Reply == "" && len(resp.ToolCalls) == 0 {
|
|
fmt.Println("(no response)")
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// 4. Streaming demonstration.
|
|
fmt.Println("─── Streaming Demo ───")
|
|
fmt.Println()
|
|
streamQ := "Explain what Go Micro is in two sentences."
|
|
fmt.Printf("Q: %s\n", streamQ)
|
|
fmt.Print("A: ")
|
|
|
|
stream, err := ag.Stream(context.Background(), streamQ)
|
|
if err != nil {
|
|
fmt.Printf("stream error: %v\n", err)
|
|
} else {
|
|
for {
|
|
chunk, err := stream.Recv()
|
|
if err != nil {
|
|
break
|
|
}
|
|
if chunk.Reply != "" {
|
|
fmt.Print(chunk.Reply)
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
fmt.Println()
|
|
fmt.Println("Done.")
|
|
}
|