Skip to content

Commit d22e184

Browse files
committed
Dont depend on symfony/ai directly : it can be used standalone or in combination.
1 parent 0eccc55 commit d22e184

13 files changed

Lines changed: 224 additions & 101 deletions

README.md

Lines changed: 40 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Perfect for building chatbots, AI agents, CLI wizards, or any system that guides
88

99
Agent Rules provides a logical processing unit for handling **unstructured input in a structured and interactive way**. It enables you to define validation rules that can request missing information, enforce business logic, and provide intelligent feedback to users or AI agents.
1010

11-
Built on top of [Symfony AI Agent](https://github.com/symfony/ai-agent), this library integrates seamlessly with AI-powered tools and conversational interfaces.
11+
Built to be compatible with [Symfony AI Agent](https://github.com/symfony/ai-agent), this library integrates seamlessly with AI-powered tools and conversational interfaces.
1212

1313
## Key Features
1414

@@ -17,7 +17,7 @@ Built on top of [Symfony AI Agent](https://github.com/symfony/ai-agent), this li
1717
- **Interactive Feedback**: Rules can request missing data with structured result types
1818
- **Composable Logic**: Combine rules with `Sequence` (AND), `Any` (OR), and `Either` patterns
1919
- **Type-Safe**: Full PHP 8.4+ type safety with readonly classes and generic support
20-
- **AI Agent Integration**: Built-in support for Symfony AI Agent's source tracking and tool system
20+
- **Framework Agnostic**: Works independently with optional Symfony AI integration
2121

2222
## Installation
2323

@@ -247,66 +247,36 @@ The engine automatically:
247247
- Detects cyclic dependencies
248248
- Validates that all dependencies exist
249249

250-
## Integration with Symfony AI Agent
250+
## Adding Context with Sources
251251

252-
### Creating an AI Tool
252+
You can attach contextual information to results that helps users or AI agents understand where to find more information:
253253

254254
```php
255-
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;
256-
use Symfony\AI\Agent\Toolbox\Source\HasSourcesInterface;
257-
use Symfony\AI\Agent\Toolbox\Source\HasSourcesTrait;
255+
use Phpro\AgentRules\Source\Source;
258256

259-
#[AsTool(
260-
name: 'validate_order',
261-
description: 'Validates customer order information and guides through missing data.'
262-
)]
263-
final class OrderValidationTool implements HasSourcesInterface
264-
{
265-
use HasSourcesTrait;
266-
267-
public function __construct(
268-
private RuleEngine $ruleEngine,
269-
) {}
270-
271-
public function __invoke(
272-
string $email,
273-
?string $productId = null,
274-
?int $quantity = null,
275-
): ResultInterface {
276-
$request = new OrderRequest($email, $productId, $quantity);
277-
$evaluation = $this->ruleEngine->evaluate($request);
278-
279-
$result = $evaluation->result ?? new CompleteResult(
280-
message: 'Order validation complete!'
281-
);
282-
283-
// Track sources for AI agent context
284-
foreach ($result->sources()->getSources() as $source) {
285-
$this->addSource($source);
286-
}
287-
288-
return $result;
289-
}
290-
}
291-
```
292-
293-
### Adding Sources for Context
257+
$result = new IncompleteResult(
258+
missingField: 'productId',
259+
message: 'Please provide the product ID.'
260+
);
294261

295-
```php
296-
return RuleEvaluation::respond(
297-
(new IncompleteResult(
298-
missingField: 'productId',
299-
message: 'Please provide the product ID.'
300-
))->addSources(
301-
new Source(
302-
name: 'Product Catalog',
303-
reference: 'https://example.com/products',
304-
content: 'Browse our product catalog to find product IDs.'
305-
)
262+
$result->sources()->add(
263+
new Source(
264+
name: 'Product Catalog',
265+
reference: 'https://example.com/products',
266+
content: 'Browse our product catalog to find product IDs.'
306267
)
307268
);
269+
270+
return RuleEvaluation::respond($result);
308271
```
309272

273+
Sources are particularly useful for:
274+
- Documentation links
275+
- Example values
276+
- Help text
277+
- Related resources
278+
- API references
279+
310280
### Symfony Configuration
311281

312282
Configure the rule engine as a service:
@@ -383,10 +353,15 @@ sequenceDiagram
383353

384354
```php
385355
// 1. Define the request context
356+
357+
use Symfony\AI\Platform\Contract\JsonSchema\Attribute\With;
358+
386359
final class RegistrationRequest
387360
{
388361
public function __construct(
389362
public readonly ?string $email = null,
363+
#[With(minLength: 10, maxLength: 255)]
364+
#[\SensitiveParameter]
390365
public readonly ?string $password = null,
391366
public readonly ?string $fullName = null,
392367
public readonly ?bool $termsAccepted = null,
@@ -469,23 +444,22 @@ final class RegistrationTool implements HasSourcesInterface
469444
) {}
470445

471446
public function __invoke(
472-
?string $email = null,
473-
?string $password = null,
474-
?string $fullName = null,
475-
?bool $termsAccepted = null,
447+
RegistrationRequest $request,
476448
): ResultInterface {
477-
$request = new RegistrationRequest(
478-
email: $email,
479-
password: $password,
480-
fullName: $fullName,
481-
termsAccepted: $termsAccepted,
482-
);
483-
484449
$evaluation = $this->ruleEngine->evaluate($request);
485-
486-
return $evaluation->result ?? new CompleteResult(
450+
$result = $evaluation->result ?? new CompleteResult(
487451
message: 'Registration validated! Your account has been created.'
488452
);
453+
454+
foreach ($result->sources() as $source) {
455+
$this->addSource(new \Symfony\AI\Agent\Toolbox\Source\Source(
456+
name: $source->name,
457+
reference: $source->reference,
458+
content: $source->content
459+
));
460+
}
461+
462+
return $result;
489463
}
490464
}
491465

composer.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
"type": "library",
55
"require": {
66
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
7-
"azjezz/psl": "^4.2",
8-
"symfony/ai-agent": "^0.2.0"
7+
"azjezz/psl": "^4.2"
98
},
109
"license": "MIT",
1110
"autoload": {

mago.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ literal-named-argument = { enabled = false }
2020
halstead = { effort-threshold = 7000 }
2121

2222
[analyzer]
23-
excludes = ["tests/**/*.php"]
23+
excludes = ["tests/"]
2424
plugins = ["psl"]
2525
find-unused-definitions = true
2626
find-unused-expressions = true

src/Result/AbstractResult.php

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
namespace Phpro\AgentRules\Result;
66

7-
use Symfony\AI\Agent\Toolbox\Source\Source;
8-
use Symfony\AI\Agent\Toolbox\Source\SourceMap;
7+
use Phpro\AgentRules\Source\SourceMap;
98

109
abstract readonly class AbstractResult implements ResultInterface
1110
{
@@ -22,15 +21,6 @@ public function sources(): SourceMap
2221
return $this->sources;
2322
}
2423

25-
public function addSources(Source ... $sources): self
26-
{
27-
foreach ($sources as $source) {
28-
$this->sources->addSource($source);
29-
}
30-
31-
return $this;
32-
}
33-
3424
#[\Override]
3525
public function jsonSerialize(): array
3626
{

src/Result/ResultInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
namespace Phpro\AgentRules\Result;
77

8-
use Symfony\AI\Agent\Toolbox\Source\SourceMap;
8+
9+
use Phpro\AgentRules\Source\SourceMap;
910

1011
interface ResultInterface extends \JsonSerializable
1112
{

src/Source/Source.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
6+
namespace Phpro\AgentRules\Source;
7+
8+
final readonly class Source
9+
{
10+
public function __construct(
11+
public string $name,
12+
public string $reference,
13+
public string $content,
14+
) {
15+
}
16+
}

src/Source/SourceMap.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
6+
namespace Phpro\AgentRules\Source;
7+
8+
final class SourceMap
9+
{
10+
/**
11+
* @var list<Source>
12+
*/
13+
public array $sources;
14+
15+
/**
16+
* @no-named-arguments
17+
*/
18+
public function __construct(
19+
Source ... $sources,
20+
) {
21+
$this->sources = $sources;
22+
}
23+
24+
/**
25+
* @return list<Source>
26+
*/
27+
public function sources(): array
28+
{
29+
return $this->sources;
30+
}
31+
32+
public function add(Source ... $sources): self
33+
{
34+
$this->sources = [...$this->sources, ...$sources];
35+
36+
return $this;
37+
}
38+
}

tests/Result/BlockedResultTest.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use PHPUnit\Framework\Attributes\Test;
99
use PHPUnit\Framework\TestCase;
1010
use Phpro\AgentRules\Result\BlockedResult;
11-
use Symfony\AI\Agent\Toolbox\Source\Source;
11+
use Phpro\AgentRules\Source\Source;
1212

1313
#[CoversClass(BlockedResult::class)]
1414
final class BlockedResultTest extends TestCase
@@ -38,7 +38,7 @@ public function it_returns_empty_source_map(): void
3838
{
3939
$result = new BlockedResult('test_reason', 'Test message');
4040

41-
$sources = $result->sources()->getSources();
41+
$sources = $result->sources()->sources();
4242

4343
static::assertCount(0, $sources);
4444
}
@@ -47,11 +47,14 @@ public function it_returns_empty_source_map(): void
4747
public function it_can_add_sources_to_result(): void
4848
{
4949
$result = new BlockedResult('test_reason', 'Test message');
50-
$source = new Source('test-source', 'ref', 'content');
50+
$source = new Source('Policy Document', 'https://policy.example.com', 'See policy for details');
5151

52-
$result->addSources($source);
52+
$result->sources()->add($source);
5353

54-
$sources = $result->sources()->getSources();
54+
$sources = $result->sources()->sources();
5555
static::assertCount(1, $sources);
56+
static::assertSame('Policy Document', $sources[0]->name);
57+
static::assertSame('https://policy.example.com', $sources[0]->reference);
58+
static::assertSame('See policy for details', $sources[0]->content);
5659
}
5760
}

tests/Result/CompleteResultTest.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use PHPUnit\Framework\Attributes\Test;
99
use PHPUnit\Framework\TestCase;
1010
use Phpro\AgentRules\Result\CompleteResult;
11-
use Symfony\AI\Agent\Toolbox\Source\Source;
11+
use Phpro\AgentRules\Source\Source;
1212

1313
#[CoversClass(CompleteResult::class)]
1414
final class CompleteResultTest extends TestCase
@@ -37,7 +37,7 @@ public function it_returns_empty_source_map(): void
3737
{
3838
$result = new CompleteResult('Task completed');
3939

40-
$sources = $result->sources()->getSources();
40+
$sources = $result->sources()->sources();
4141

4242
static::assertCount(0, $sources);
4343
}
@@ -46,11 +46,14 @@ public function it_returns_empty_source_map(): void
4646
public function it_can_add_sources_to_result(): void
4747
{
4848
$result = new CompleteResult('Task completed');
49-
$source = new Source('test-source', 'ref', 'content');
49+
$source = new Source('Documentation', 'https://docs.example.com', 'See the docs for more info');
5050

51-
$result->addSources($source);
51+
$result->sources()->add($source);
5252

53-
$sources = $result->sources()->getSources();
53+
$sources = $result->sources()->sources();
5454
static::assertCount(1, $sources);
55+
static::assertSame('Documentation', $sources[0]->name);
56+
static::assertSame('https://docs.example.com', $sources[0]->reference);
57+
static::assertSame('See the docs for more info', $sources[0]->content);
5558
}
5659
}

tests/Result/ErrorResultTest.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use PHPUnit\Framework\Attributes\Test;
99
use PHPUnit\Framework\TestCase;
1010
use Phpro\AgentRules\Result\ErrorResult;
11-
use Symfony\AI\Agent\Toolbox\Source\Source;
11+
use Phpro\AgentRules\Source\Source;
1212

1313
#[CoversClass(ErrorResult::class)]
1414
final class ErrorResultTest extends TestCase
@@ -41,7 +41,7 @@ public function it_returns_empty_source_map(): void
4141
{
4242
$result = new ErrorResult('Error occurred', 'Fix the issue');
4343

44-
$sources = $result->sources()->getSources();
44+
$sources = $result->sources()->sources();
4545

4646
static::assertCount(0, $sources);
4747
}
@@ -50,11 +50,14 @@ public function it_returns_empty_source_map(): void
5050
public function it_can_add_sources_to_result(): void
5151
{
5252
$result = new ErrorResult('Error occurred', 'Fix the issue');
53-
$source = new Source('test-source', 'ref', 'content');
53+
$source = new Source('Error Log', 'https://logs.example.com', 'Stack trace details');
5454

55-
$result->addSources($source);
55+
$result->sources()->add($source);
5656

57-
$sources = $result->sources()->getSources();
57+
$sources = $result->sources()->sources();
5858
static::assertCount(1, $sources);
59+
static::assertSame('Error Log', $sources[0]->name);
60+
static::assertSame('https://logs.example.com', $sources[0]->reference);
61+
static::assertSame('Stack trace details', $sources[0]->content);
5962
}
6063
}

0 commit comments

Comments
 (0)