From f8fa7e7c7b2f141e6f3015be59f02df05d03e1b6 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 23:04:05 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20agregar=20tool=20imdb=5Fsearch=20para?= =?UTF-8?q?=20buscar=20pel=C3=ADculas=20en=20IMDb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementa una nueva tool que permite buscar películas y series en IMDb usando la API de OMDb. Retorna hasta 5 resultados con título, año, tipo, poster URL e IMDb ID. Cambios: - tools/imdb/imdb.go: tool imdb_search con integración a OMDb API - internal/config/schema.go: IMDbToolCfg con api_key, api_key_env y timeout - agents/runtime.go: registro de tool en buildToolRegistry - agents/asistente-2/config.yaml: habilitación de tool imdb - .env.example: OMDB_API_KEY para configuración La tool soporta parámetros: - query (requerido): título de película/serie a buscar - year (opcional): año para filtrar resultados Configuración via api_key directa o variable de entorno OMDB_API_KEY. API key gratuita disponible en http://www.omdbapi.com/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .env.example | 4 + agents/asistente-2/config.yaml | 6 ++ agents/runtime.go | 7 ++ internal/config/schema.go | 8 ++ tools/imdb/imdb.go | 167 +++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 tools/imdb/imdb.go diff --git a/.env.example b/.env.example index ec48a4a..6bfdc60 100644 --- a/.env.example +++ b/.env.example @@ -34,6 +34,10 @@ SSSS_RECOVERY_KEY_DEVOPS_BOT= OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... # opcional, para cuando añadas el devops-bot con Claude +# ── External APIs ──────────────────────────────────────────── +# OMDb API key para búsqueda de películas en IMDb (obtener de http://www.omdbapi.com/) +OMDB_API_KEY= + # ── SSH (para devops-bot, cuando lo añadas) ────────────────── SSH_PRIVATE_KEY_PATH=/home/ubuntu/.ssh/id_ed25519 SSH_MONITOR_KEY_PATH=/home/ubuntu/.ssh/id_ed25519 diff --git a/agents/asistente-2/config.yaml b/agents/asistente-2/config.yaml index 92192e4..22bf4f6 100644 --- a/agents/asistente-2/config.yaml +++ b/agents/asistente-2/config.yaml @@ -128,6 +128,12 @@ tools: knowledge: enabled: true + imdb: + enabled: true + api_key: "" + api_key_env: "OMDB_API_KEY" + timeout: 10s + # ============================================ # MEMORIA — ventana de conversación + hechos # ============================================ diff --git a/agents/runtime.go b/agents/runtime.go index 8ceb228..a47c1de 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -37,6 +37,7 @@ import ( toolclock "github.com/enmanuel/agents/tools/clock" toolfile "github.com/enmanuel/agents/tools/file" toolhttp "github.com/enmanuel/agents/tools/http" + toolimdb "github.com/enmanuel/agents/tools/imdb" toolknowledge "github.com/enmanuel/agents/tools/knowledgetools" toolmatrix "github.com/enmanuel/agents/tools/matrix" toolmcp "github.com/enmanuel/agents/tools/mcptools" @@ -1104,6 +1105,12 @@ func buildToolRegistry( reg.Register(toolweather.NewWeather()) logger.Debug("registered weather tool") + // imdb tool (enabled via config) + if cfg.Tools.IMDb.Enabled { + reg.Register(toolimdb.NewIMDbSearch(cfg.Tools.IMDb)) + logger.Debug("registered imdb tool") + } + // matrix_send is always available reg.Register(toolmatrix.NewMatrixSend(matrixClient, cfg.Tools.Matrix)) logger.Debug("registered matrix tool") diff --git a/internal/config/schema.go b/internal/config/schema.go index c73f363..48cb316 100644 --- a/internal/config/schema.go +++ b/internal/config/schema.go @@ -158,6 +158,7 @@ type ToolsCfg struct { Knowledge KnowledgeToolCfg `yaml:"knowledge"` SharedKnowledge SharedKnowledgeToolCfg `yaml:"shared_knowledge"` Skills SkillsToolCfg `yaml:"skills"` + IMDb IMDbToolCfg `yaml:"imdb"` } type MatrixToolCfg struct { @@ -519,6 +520,13 @@ type SkillsToolCfg struct { AllowedInterpreters []string `yaml:"allowed_interpreters"` // allowlist for skill script execution (default: ["bash", "sh"]) } +type IMDbToolCfg struct { + Enabled bool `yaml:"enabled"` + APIKey string `yaml:"api_key"` // OMDb API key (get from http://www.omdbapi.com/) + APIKeyEnv string `yaml:"api_key_env"` // env var name for API key (e.g., "OMDB_API_KEY") + Timeout time.Duration `yaml:"timeout"` // timeout for API requests (default: 10s) +} + // ── Special Agents ──────────────────────────────────────────────────────── // SpecialConfig is the root configuration for a special agent (no Matrix identity). diff --git a/tools/imdb/imdb.go b/tools/imdb/imdb.go new file mode 100644 index 0000000..74adf9e --- /dev/null +++ b/tools/imdb/imdb.go @@ -0,0 +1,167 @@ +package imdb + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/enmanuel/agents/internal/config" + "github.com/enmanuel/agents/tools" +) + +// SearchResult represents a single movie/series result from OMDb API. +type SearchResult struct { + Title string `json:"Title"` + Year string `json:"Year"` + ImdbID string `json:"imdbID"` + Type string `json:"Type"` + Poster string `json:"Poster"` +} + +// SearchResponse represents the full response from OMDb search endpoint. +type SearchResponse struct { + Search []SearchResult `json:"Search"` + TotalResults string `json:"totalResults"` + Response string `json:"Response"` + Error string `json:"Error"` +} + +// NewIMDbSearch creates an imdb_search tool that searches movies on IMDb via OMDb API. +// Returns up to 5 results with title, year, type, poster URL, and IMDb ID. +// Requires API key from http://www.omdbapi.com/ +func NewIMDbSearch(cfg config.IMDbToolCfg) tools.Tool { + timeout := cfg.Timeout + if timeout == 0 { + timeout = 10 * time.Second + } + client := &http.Client{Timeout: timeout} + + return tools.Tool{ + Def: tools.Def{ + Name: "imdb_search", + Description: "Search for movies or series on IMDb by title. Returns up to 5 results with title, year, type, poster image URL, and IMDb ID.", + Parameters: []tools.Param{ + {Name: "query", Type: "string", Description: "The movie or series title to search for", Required: true}, + {Name: "year", Type: "integer", Description: "Optional year to filter results (e.g., 2020)", Required: false}, + }, + }, + Exec: func(ctx context.Context, args map[string]any) tools.Result { + query := tools.GetString(args, "query") + if query == "" { + return tools.Result{Err: fmt.Errorf("imdb_search: query is required")} + } + + // Get API key from config or env var + apiKey := cfg.APIKey + if apiKey == "" && cfg.APIKeyEnv != "" { + apiKey = getEnvVar(cfg.APIKeyEnv) + } + if apiKey == "" { + return tools.Result{Err: fmt.Errorf("imdb_search: API key not configured (set imdb.api_key or imdb.api_key_env in config)")} + } + + // Build search URL + searchURL := buildSearchURL(apiKey, query, args) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil) + if err != nil { + return tools.Result{Err: fmt.Errorf("imdb_search: %w", err)} + } + + resp, err := client.Do(req) + if err != nil { + return tools.Result{Err: fmt.Errorf("imdb_search: %w", err)} + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return tools.Result{Err: fmt.Errorf("imdb_search: HTTP %d", resp.StatusCode)} + } + + body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) + if err != nil { + return tools.Result{Err: fmt.Errorf("imdb_search: read body: %w", err)} + } + + var searchResp SearchResponse + if err := json.Unmarshal(body, &searchResp); err != nil { + return tools.Result{Err: fmt.Errorf("imdb_search: parse response: %w", err)} + } + + if searchResp.Response == "False" { + return tools.Result{Output: fmt.Sprintf("No se encontraron resultados para '%s'. Error: %s", query, searchResp.Error)} + } + + // Format results (limit to first 5) + output := formatResults(searchResp.Search, query) + return tools.Result{Output: output} + }, + } +} + +// buildSearchURL constructs the OMDb API search URL with query parameters. +func buildSearchURL(apiKey, query string, args map[string]any) string { + params := url.Values{} + params.Set("apikey", apiKey) + params.Set("s", query) + params.Set("type", "movie") // default to movies, could be made configurable + + // Add year filter if provided + if year := tools.GetInt(args, "year"); year > 0 { + params.Set("y", fmt.Sprintf("%d", year)) + } + + return fmt.Sprintf("https://www.omdbapi.com/?%s", params.Encode()) +} + +// formatResults converts search results into a readable text format. +func formatResults(results []SearchResult, query string) string { + if len(results) == 0 { + return fmt.Sprintf("No se encontraron películas para '%s'", query) + } + + var builder strings.Builder + builder.WriteString(fmt.Sprintf("🎬 Resultados de IMDb para '%s':\n\n", query)) + + // Limit to 5 results + limit := 5 + if len(results) < limit { + limit = len(results) + } + + for i := 0; i < limit; i++ { + r := results[i] + builder.WriteString(fmt.Sprintf("%d. **%s** (%s)\n", i+1, r.Title, r.Year)) + builder.WriteString(fmt.Sprintf(" • Tipo: %s\n", r.Type)) + builder.WriteString(fmt.Sprintf(" • IMDb ID: %s\n", r.ImdbID)) + + if r.Poster != "" && r.Poster != "N/A" { + builder.WriteString(fmt.Sprintf(" • Poster: %s\n", r.Poster)) + } else { + builder.WriteString(" • Poster: No disponible\n") + } + + builder.WriteString(fmt.Sprintf(" • Link: https://www.imdb.com/title/%s/\n", r.ImdbID)) + + if i < limit-1 { + builder.WriteString("\n") + } + } + + if len(results) > 5 { + builder.WriteString(fmt.Sprintf("\n... y %d resultado(s) más", len(results)-5)) + } + + return builder.String() +} + +// getEnvVar retrieves an environment variable by name. +func getEnvVar(name string) string { + return os.Getenv(name) +}