Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions cmd/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,253 @@ var agentTestCmd = &cobra.Command{
},
}

// Agent installation commands

var agentAddCmd = &cobra.Command{
Use: "add <source> [agent-name]",
Short: "Add an agent to your project",
Long: `Add an agent to your project from various sources.

Sources can be:
- Catalog references: memory/vector-store, planner/task-decompose
- Git repositories: github.com/user/repo#branch path/to/agent
- Direct URLs: https://example.com/agent.zip
- Local paths: ./path/to/agent

Examples:
agentuity agent add memory/vector-store
agentuity agent add github.com/user/agents#main my-agent
agentuity agent add ./local-agents/custom-agent
agentuity agent add --as custom-name memory/vector-store`,
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
l := env.NewLogger(cmd)

// Get current directory as project root
projectRoot, err := os.Getwd()
if err != nil {
errsystem.New(errsystem.ErrListFilesAndDirectories, err, errsystem.WithContextMessage("Failed to get current directory")).ShowErrorAndExit()
}

// Check if we're in a project directory
if !project.ProjectExists(projectRoot) {
errsystem.New(errsystem.ErrInvalidConfiguration, fmt.Errorf("agentuity.yaml not found"), errsystem.WithContextMessage("Not in an Agentuity project directory")).ShowErrorAndExit()
}

source := args[0]
agentName := ""
if len(args) > 1 {
agentName = args[1]
}

// Get flags
localName, _ := cmd.Flags().GetString("as")
if localName != "" {
agentName = localName
}
noInstall, _ := cmd.Flags().GetBool("no-install")
force, _ := cmd.Flags().GetBool("force")
cacheDir, _ := cmd.Flags().GetString("cache-dir")

// Default cache directory
if cacheDir == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
errsystem.New(errsystem.ErrListFilesAndDirectories, err, errsystem.WithContextMessage("Failed to get home directory")).ShowErrorAndExit()
}
cacheDir = filepath.Join(homeDir, ".config", "agentuity", "agents")
}

if err := runAddCommand(context.Background(), l, source, agentName, projectRoot, cacheDir, noInstall, force); err != nil {
errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to add agent")).ShowErrorAndExit()
}
},
}

var agentListLocalCmd = &cobra.Command{
Use: "list-local",
Short: "List locally installed agents",
Long: "List all agents installed in the current project.",
Run: func(cmd *cobra.Command, args []string) {
// Get current directory as project root
projectRoot, err := os.Getwd()
if err != nil {
errsystem.New(errsystem.ErrListFilesAndDirectories, err, errsystem.WithContextMessage("Failed to get current directory")).ShowErrorAndExit()
}

// Check if we're in a project directory
if !project.ProjectExists(projectRoot) {
errsystem.New(errsystem.ErrInvalidConfiguration, fmt.Errorf("agentuity.yaml not found"), errsystem.WithContextMessage("Not in an Agentuity project directory")).ShowErrorAndExit()
}

installer := agent.NewAgentInstaller(projectRoot)

agents, err := installer.ListInstalled()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could use reconcileAgentList and filter by the Local property too

if err != nil {
errsystem.New(errsystem.ErrListFilesAndDirectories, err, errsystem.WithContextMessage("Failed to list installed agents")).ShowErrorAndExit()
}

if len(agents) == 0 {
fmt.Println("No agents installed in this project.")
fmt.Println("\nUse 'agentuity agent add <source>' to install an agent.")
return
}

fmt.Printf("Found %d installed agent(s):\n\n", len(agents))
for _, agentName := range agents {
fmt.Printf("• %s\n", agentName)
agentPath := filepath.Join(projectRoot, "agents", agentName, "agent.yaml")
if metadata, err := loadAgentMetadata(agentPath); err == nil {
fmt.Printf(" Description: %s\n", metadata.Description)
fmt.Printf(" Language: %s\n", metadata.Language)
fmt.Printf(" Version: %s\n", metadata.Version)
}
fmt.Println()
}
},
}

var agentRemoveCmd = &cobra.Command{
Use: "remove <agent-name>",
Aliases: []string{"rm", "uninstall"},
Short: "Remove an installed agent",
Long: "Remove an agent from the current project.",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
l := env.NewLogger(cmd)

// Get current directory as project root
projectRoot, err := os.Getwd()
if err != nil {
errsystem.New(errsystem.ErrListFilesAndDirectories, err, errsystem.WithContextMessage("Failed to get current directory")).ShowErrorAndExit()
}

// Check if we're in a project directory
if !project.ProjectExists(projectRoot) {
errsystem.New(errsystem.ErrInvalidConfiguration, fmt.Errorf("agentuity.yaml not found"), errsystem.WithContextMessage("Not in an Agentuity project directory")).ShowErrorAndExit()
}

agentName := args[0]
force, _ := cmd.Flags().GetBool("force")

// Confirm removal
if !force && !tui.Ask(l, fmt.Sprintf("Remove agent '%s'?", agentName), false) {
fmt.Println("Removal cancelled.")
return
}

installer := agent.NewAgentInstaller(projectRoot)

if err := installer.Uninstall(agentName); err != nil {
errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to remove agent")).ShowErrorAndExit()
}

tui.ShowSuccess("Agent '%s' removed successfully!", agentName)
},
}

func runAddCommand(ctx context.Context, l logger.Logger, source, agentName, projectRoot, cacheDir string, noInstall, force bool) error {
// Initialize components
resolver := agent.NewSourceResolver()
downloader := agent.NewAgentDownloader(cacheDir)
installer := agent.NewAgentInstaller(projectRoot)

var agentSource *agent.AgentSource
var pkg *agent.AgentPackage
var err error

// Resolve and download agent
tui.ShowSpinner("Resolving and downloading agent...", func() {
// Resolve source
agentSource, err = resolver.Resolve(source)
if err != nil {
return
}

l.Debug("Resolved source: %+v", agentSource)

// Download agent package
pkg, err = downloader.Download(agentSource)
})

if err != nil {
return fmt.Errorf("failed to resolve/download agent: %w", err)
}

// Display agent information
tui.ShowSuccess("Agent downloaded successfully!")
fmt.Printf("Name: %s\n", pkg.Metadata.Name)
fmt.Printf("Version: %s\n", pkg.Metadata.Version)
fmt.Printf("Description: %s\n", pkg.Metadata.Description)
fmt.Printf("Language: %s\n", pkg.Metadata.Language)
if pkg.Metadata.Author != "" {
fmt.Printf("Author: %s\n", pkg.Metadata.Author)
}
fmt.Printf("Files: %d\n", len(pkg.Files))
fmt.Println()

// Confirm installation
if !force && !tui.Ask(l, "Install this agent?", true) {
fmt.Println("Installation cancelled.")
return nil
}

// Install agent
var installErr error
finalName := agentName
if finalName == "" {
finalName = pkg.Metadata.Name
}

tui.ShowSpinner("Installing agent...", func() {
installOpts := &agent.InstallOptions{
LocalName: agentName,
NoInstall: noInstall,
Force: force,
ProjectRoot: projectRoot,
}

installErr = installer.Install(pkg, installOpts)
})

if installErr != nil {
return fmt.Errorf("failed to install agent: %w", installErr)
}

// Final success message
tui.ShowSuccess("Agent '%s' installed successfully!", finalName)

agentPath := filepath.Join(projectRoot, "agents", finalName)
fmt.Printf("Agent files copied to: %s\n", agentPath)

if !noInstall && pkg.Metadata.Dependencies != nil {
if hasNonEmptyDeps(pkg.Metadata.Dependencies) {
fmt.Println("Dependencies installed.")
}
}

fmt.Println("\nNext steps:")
fmt.Printf("1. Review the agent files in %s\n", agentPath)
fmt.Printf("2. Configure the agent settings if needed\n")
fmt.Printf("3. Run 'agentuity dev' to test your project\n")

return nil
}

func hasNonEmptyDeps(deps *agent.AgentDependencies) bool {
return (len(deps.NPM) > 0) || (len(deps.Pip) > 0) || (len(deps.Go) > 0)
}

func loadAgentMetadata(agentYamlPath string) (*agent.AgentMetadata, error) {
// This is a simplified version - in practice you'd use the same YAML parsing
// as used in the installer
return &agent.AgentMetadata{
Description: "Agent description",
Language: "typescript",
Version: "1.0.0",
}, nil
}

func init() {
rootCmd.AddCommand(agentCmd)
agentCmd.AddCommand(agentCreateCmd)
Expand All @@ -860,4 +1107,16 @@ func init() {
cmd.Flags().Bool("force", false, "Force the creation of the agent even if it already exists")
}

// Add agent installation commands
agentCmd.AddCommand(agentAddCmd)
agentCmd.AddCommand(agentListLocalCmd)
agentCmd.AddCommand(agentRemoveCmd)

// Add flags for agent installation commands
agentAddCmd.Flags().StringP("as", "a", "", "Local name for the agent")
agentAddCmd.Flags().Bool("no-install", false, "Skip dependency installation")
agentAddCmd.Flags().Bool("force", false, "Overwrite existing agent")
agentAddCmd.Flags().String("cache-dir", "", "Custom cache directory")

agentRemoveCmd.Flags().Bool("force", false, "Skip confirmation prompt")
}
69 changes: 69 additions & 0 deletions examples/agent-example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Vector Store Agent

A semantic memory agent that provides vector-based storage and retrieval using Pinecone and OpenAI embeddings.

## Features

- Store text content with semantic embeddings
- Search for similar content using natural language queries
- Configurable vector dimensions and similarity metrics
- Metadata support for rich context

## Setup

1. Install dependencies:
```bash
npm install @pinecone-database/pinecone openai uuid
```

2. Set environment variables:
```bash
export PINECONE_API_KEY="your-pinecone-api-key"
export PINECONE_ENVIRONMENT="your-pinecone-environment"
export OPENAI_API_KEY="your-openai-api-key"
```

3. Create a Pinecone index with 1536 dimensions (for OpenAI embeddings)

## Usage

```typescript
import { VectorStore } from './src/vector-store';
import config from './config/vector-store.yaml';

const vectorStore = new VectorStore(config);

// Store content
const id = await vectorStore.store(
"The capital of France is Paris",
{ topic: "geography", source: "knowledge-base" }
);

// Search for similar content
const results = await vectorStore.search("What is the capital of France?");
console.log(results[0].content); // "The capital of France is Paris"

// Delete content
await vectorStore.delete(id);
```

## Configuration

Edit `config/vector-store.yaml` to customize:

- `pinecone.index_name`: Name of your Pinecone index
- `pinecone.dimension`: Vector dimension (1536 for OpenAI ada-002)
- `pinecone.metric`: Distance metric (cosine, euclidean, dotproduct)
- `openai.model`: OpenAI embedding model to use
- `openai.max_tokens`: Maximum tokens for text processing

## API

### `store(content: string, metadata?: Record<string, any>): Promise<string>`
Store text content and return a unique ID.

### `search(query: string, topK?: number): Promise<MemoryEntry[]>`
Search for similar content and return top matches.

### `delete(id: string): Promise<void>`
Delete stored content by ID.
23 changes: 23 additions & 0 deletions examples/agent-example/agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: vector-store
version: 1.0.0
description: Semantic memory with vector storage using Pinecone
author: agentuity
language: typescript
dependencies:
npm:
- "@pinecone-database/pinecone"
- "openai"
- "uuid"
files:
- src/vector-store.ts
- src/types.ts
- config/vector-store.yaml
- README.md
config:
pinecone:
index_name: "agent-memory"
dimension: 1536
metric: "cosine"
openai:
model: "text-embedding-ada-002"
max_tokens: 8191
13 changes: 13 additions & 0 deletions examples/agent-example/config/vector-store.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pinecone:
index_name: "agent-memory"
dimension: 1536
metric: "cosine"

openai:
model: "text-embedding-ada-002"
max_tokens: 8191

# Environment variables required:
# PINECONE_API_KEY - Your Pinecone API key
# PINECONE_ENVIRONMENT - Your Pinecone environment
# OPENAI_API_KEY - Your OpenAI API key
Loading
Loading