Skip to content

Commit

Permalink
Agent creation and chat commands
Browse files Browse the repository at this point in the history
  • Loading branch information
MaestroError committed Feb 2, 2025
1 parent 76c1804 commit ac4d195
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 23 deletions.
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ LarAgent brings the power of AI agents to your Laravel projects with an elegant,
- [Creating an Agent](#creating-an-agent)
- [Using Tools](#using-tools)
- [Managing Chat History](#managing-chat-history)
- [Commands](#commands)
- [Creating an Agent](#creating-an-agent-1)
- [Interactive Chat](#interactive-chat)
- [Advanced Usage](#advanced-usage)
- [Custom Agents](#custom-agents)
- [Custom Tools](#custom-tools)
Expand Down Expand Up @@ -143,7 +146,6 @@ Structured output is used to define the format (JSON Schema) of the output gener

### Usage in and outside of Laravel

// @todo add agent creation command here
// @todo add link to usage out of laravel documentation


Expand Down Expand Up @@ -198,8 +200,37 @@ You can manage chat history by using agent class per key or user.

// @todo add chat history management methods

## Advanced Usage
## Commands

### Creating an Agent

You can quickly create a new agent using the `make:agent` command:

```bash
php artisan make:agent WeatherAgent
```

This will create a new agent class in your `app/AiAgents` directory with the basic structure and methods needed to get started.

### Interactive Chat

You can start an interactive chat session with any of your agents using the `agent:chat` command:

```bash
# Start a chat with default history name
php artisan agent:chat WeatherAgent

# Start a chat with a specific history name
php artisan agent:chat WeatherAgent --history=weather_chat_1
```

The chat session allows you to:
- Send messages to your agent
- Get responses in real-time
- Use any tools configured for the agent
- Type 'exit' to end the chat session

## Advanced Usage

### Ai agents as Tools

Expand Down
8 changes: 8 additions & 0 deletions logs/agent-chat-errors.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[2025-02-02 09:01:15] Agent not found: NonExistentAgent
[2025-02-02 09:01:55] Agent not found: NonExistentAgent
[2025-02-02 09:03:42] Agent not found: NonExistentAgent
[2025-02-02 09:04:09] Agent not found: NonExistentAgent
[2025-02-02 09:04:40] Agent not found: NonExistentAgent
[2025-02-02 09:05:31] Agent not found: NonExistentAgent
[2025-02-02 09:06:24] Agent not found: NonExistentAgent
[2025-02-02 09:06:51] Agent not found: NonExistentAgent
68 changes: 68 additions & 0 deletions src/Commands/AgentChatCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

namespace LarAgent\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;

class AgentChatCommand extends Command
{
protected $signature = 'agent:chat {agent : The name of the agent to chat with} {--history= : Chat history name}';

protected $description = 'Start an interactive chat session with an agent';

protected function logError($message)
{
$logPath = 'logs/agent-chat-errors.log';
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] {$message}\n";

if (!is_dir(dirname($logPath))) {
mkdir(dirname($logPath), 0755, true);
}

file_put_contents($logPath, $logMessage, FILE_APPEND);
}

public function handle()
{
$agentName = $this->argument('agent');
$historyName = $this->option('history') ?? Str::random(10);

// Try both namespaces
$agentClass = "\\App\\AiAgents\\{$agentName}";
if (!class_exists($agentClass)) {
$agentClass = "\\App\\Agents\\{$agentName}";
if (!class_exists($agentClass)) {
$this->error("Agent not found: {$agentName}");
$this->logError("Agent not found: {$agentName}");
return 1;
}
}

$agent = $agentClass::for($historyName);

$this->info("Starting chat with {$agentName}");
$this->line("Using history: {$historyName}");
$this->line("Type 'exit' to end the chat\n");

while (true) {
$message = $this->ask('You');

if ($message === null || strtolower($message) === 'exit') {
$this->info("Chat ended");
return 0;
}

try {
$response = $agent->respond($message);
$this->line("\n<comment>{$agentName}:</comment>");
$this->line($response."\n");
} catch (\Exception $e) {
$this->error("Error: ".$e->getMessage());
$this->logError("Error in {$agentName} response: ".$e->getMessage()."\nStack trace:\n".$e->getTraceAsString());
return 1;
}
}
}
}
19 changes: 0 additions & 19 deletions src/Commands/LarAgentCommand.php

This file was deleted.

40 changes: 40 additions & 0 deletions src/Commands/MakeAgentCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace LarAgent\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Str;

class MakeAgentCommand extends Command
{
protected $signature = 'make:agent {name : The name of the agent}';

protected $description = 'Create a new LarAgent agent class';

public function handle()
{
$name = $this->argument('name');

$path = app_path('AiAgents/'.$name.'.php');

if (file_exists($path)) {
$this->error('Agent already exists: '.$name);
return 1;
}

$stub = file_get_contents(__DIR__.'/stubs/agent.stub');

$stub = str_replace('{{ class }}', $name, $stub);

if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0755, true);
}

file_put_contents($path, $stub);

$this->info('Agent created successfully: '.$name);
$this->line('Location: '.$path);

return 0;
}
}
26 changes: 26 additions & 0 deletions src/Commands/stubs/agent.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace App\AiAgents;

use LarAgent\Agent;

class {{ class }} extends Agent
{
protected $model = 'gpt-4';

protected $history = 'in_memory';

protected $provider = 'default';

protected $tools = [];

public function instructions()
{
return "Define your agent's instructions here.";
}

public function prompt($message)
{
return $message;
}
}
8 changes: 6 additions & 2 deletions src/LarAgentServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

namespace LarAgent;

use LarAgent\Commands\LarAgentCommand;
use LarAgent\Commands\MakeAgentCommand;
use LarAgent\Commands\AgentChatCommand;
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;

Expand All @@ -18,6 +19,9 @@ public function configurePackage(Package $package): void
$package
->name('laragent')
->hasConfigFile()
->hasCommand(LarAgentCommand::class);
->hasCommands([
MakeAgentCommand::class,
AgentChatCommand::class
]);
}
}
98 changes: 98 additions & 0 deletions tests/Commands/AgentChatCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

use LarAgent\Commands\AgentChatCommand;
use LarAgent\Agent;
use LarAgent\Tests\Fakes\FakeLlmDriver;

beforeEach(function () {
// Create a mock agent class file
if (!is_dir(app_path('AiAgents'))) {
mkdir(app_path('AiAgents'), 0755, true);
}

$agentContent = <<<'PHP'
<?php
namespace App\AiAgents;
use LarAgent\Agent;
use LarAgent\Tests\Fakes\FakeLlmDriver;
class TestAgent extends Agent
{
protected $model = 'gpt-4-mini';
protected $history = 'in_memory';
protected $provider = 'default';
protected $tools = [];
protected $driver = FakeLlmDriver::class;
public function instructions()
{
return "Test agent instructions";
}
public function prompt($message)
{
return $message;
}
protected function onInitialize()
{
$this->llmDriver->addMockResponse('stop', [
'content' => 'Test response',
]);
}
}
PHP;

file_put_contents(app_path('AiAgents/TestAgent.php'), $agentContent);

// Make sure the autoloader can find our test agent
require_once app_path('AiAgents/TestAgent.php');
});

afterEach(function () {
// Clean up the test agent
if (file_exists(app_path('AiAgents/TestAgent.php'))) {
unlink(app_path('AiAgents/TestAgent.php'));
}

if (is_dir(app_path('AiAgents')) && count(scandir(app_path('AiAgents'))) <= 2) {
rmdir(app_path('AiAgents'));
}
});

test('it fails when agent does not exist', function () {
$this->artisan('agent:chat', ['agent' => 'NonExistentAgent'])
->assertFailed()
->expectsOutput('Agent not found: NonExistentAgent');
});

test('it can start chat with existing agent', function () {
$this->artisan('agent:chat', ['agent' => 'TestAgent'])
->expectsOutput('Starting chat with TestAgent')
->expectsQuestion('You', 'exit')
->expectsOutput('Chat ended')
->assertExitCode(0);
});

test('it uses provided history name', function () {
$this->artisan('agent:chat', [
'agent' => 'TestAgent',
'--history' => 'test_history'
])
->expectsOutput('Using history: test_history')
->expectsQuestion('You', 'exit')
->expectsOutput('Chat ended')
->assertExitCode(0);
});

test('it can handle multiple messages', function () {
$this->artisan('agent:chat', ['agent' => 'TestAgent'])
->expectsOutput('Starting chat with TestAgent')
->expectsQuestion('You', 'Hello')
->expectsOutputToContain('Test response')
->expectsQuestion('You', 'exit')
->expectsOutput('Chat ended')
->assertExitCode(0);
});
Loading

0 comments on commit ac4d195

Please sign in to comment.