Skip to content

Commit 863bb29

Browse files
Add embeddings, vector search, and provider failover (v0.6)
- EmbeddingProviderInterface: embed(string|array) returning EmbeddingResponse - VectorStoreInterface: upsert, query, delete, flush, count - InMemoryVectorStore: pure PHP cosine similarity, metadata filtering - FailoverProvider: auto-retry across provider chain with configurable exception types - SearchResult and EmbeddingResponse value objects - Comprehensive tests for all new features
1 parent 4273159 commit 863bb29

10 files changed

Lines changed: 954 additions & 0 deletions
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PapiAI,
5+
* A simple but powerful PHP library for building AI agents.
6+
*
7+
* (c) Marcello Duarte <marcello.duarte@gmail.com>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace PapiAI\Core\Contracts;
16+
17+
use PapiAI\Core\EmbeddingResponse;
18+
19+
interface EmbeddingProviderInterface
20+
{
21+
/**
22+
* Generate embeddings for the given input(s).
23+
*
24+
* @param string|array<string> $input One or more texts to embed
25+
* @param array{
26+
* model?: string,
27+
* dimensions?: int,
28+
* } $options Provider-specific options
29+
*/
30+
public function embed(string|array $input, array $options = []): EmbeddingResponse;
31+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PapiAI,
5+
* A simple but powerful PHP library for building AI agents.
6+
*
7+
* (c) Marcello Duarte <marcello.duarte@gmail.com>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace PapiAI\Core\Contracts;
16+
17+
use PapiAI\Core\SearchResult;
18+
19+
interface VectorStoreInterface
20+
{
21+
/**
22+
* Insert or update a vector with metadata.
23+
*
24+
* @param string $id Unique document identifier
25+
* @param array<float> $vector The embedding vector
26+
* @param array<string, mixed> $metadata Associated metadata
27+
* @param string|null $content Original text content to store alongside
28+
*/
29+
public function upsert(string $id, array $vector, array $metadata = [], ?string $content = null): void;
30+
31+
/**
32+
* Query for similar vectors.
33+
*
34+
* @param array<float> $vector The query vector
35+
* @param int $topK Maximum number of results
36+
* @param array<string, mixed> $filter Metadata filter criteria
37+
* @return array<SearchResult>
38+
*/
39+
public function query(array $vector, int $topK = 5, array $filter = []): array;
40+
41+
/**
42+
* Delete a vector by ID.
43+
*/
44+
public function delete(string $id): void;
45+
46+
/**
47+
* Flush all vectors from the store.
48+
*/
49+
public function flush(): void;
50+
51+
/**
52+
* Get the number of stored vectors.
53+
*/
54+
public function count(): int;
55+
}

src/EmbeddingResponse.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PapiAI,
5+
* A simple but powerful PHP library for building AI agents.
6+
*
7+
* (c) Marcello Duarte <marcello.duarte@gmail.com>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace PapiAI\Core;
16+
17+
final class EmbeddingResponse
18+
{
19+
/**
20+
* @param array<array<float>> $embeddings The embedding vectors
21+
* @param string $model The model used
22+
* @param array{prompt_tokens?: int, total_tokens?: int} $usage Token usage
23+
*/
24+
public function __construct(
25+
public readonly array $embeddings,
26+
public readonly string $model,
27+
public readonly array $usage = [],
28+
) {
29+
}
30+
31+
/**
32+
* Get the first embedding vector (convenience for single-input requests).
33+
*
34+
* @return array<float>
35+
*/
36+
public function first(): array
37+
{
38+
return $this->embeddings[0] ?? [];
39+
}
40+
41+
/**
42+
* Get the number of embeddings.
43+
*/
44+
public function count(): int
45+
{
46+
return count($this->embeddings);
47+
}
48+
49+
/**
50+
* Get the dimensionality of the embeddings.
51+
*/
52+
public function dimensions(): int
53+
{
54+
return count($this->first());
55+
}
56+
57+
/**
58+
* Get prompt token count.
59+
*/
60+
public function getPromptTokens(): int
61+
{
62+
return $this->usage['prompt_tokens'] ?? 0;
63+
}
64+
65+
/**
66+
* Get total token count.
67+
*/
68+
public function getTotalTokens(): int
69+
{
70+
return $this->usage['total_tokens'] ?? 0;
71+
}
72+
}

src/FailoverProvider.php

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PapiAI,
5+
* A simple but powerful PHP library for building AI agents.
6+
*
7+
* (c) Marcello Duarte <marcello.duarte@gmail.com>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace PapiAI\Core;
16+
17+
use PapiAI\Core\Contracts\EmbeddingProviderInterface;
18+
use PapiAI\Core\Contracts\ProviderInterface;
19+
use RuntimeException;
20+
use Throwable;
21+
22+
final class FailoverProvider implements ProviderInterface, EmbeddingProviderInterface
23+
{
24+
/** @var array<ProviderInterface> */
25+
private array $providers;
26+
27+
private ?ProviderInterface $lastUsedProvider = null;
28+
29+
/**
30+
* @param array<ProviderInterface> $providers Ordered list of providers to try
31+
* @param array<class-string<Throwable>> $retryOn Exception types that trigger failover (empty = all)
32+
*/
33+
public function __construct(
34+
array $providers,
35+
private readonly array $retryOn = [],
36+
) {
37+
if (count($providers) < 2) {
38+
throw new \InvalidArgumentException('FailoverProvider requires at least 2 providers');
39+
}
40+
41+
$this->providers = array_values($providers);
42+
}
43+
44+
public function chat(array $messages, array $options = []): Response
45+
{
46+
return $this->tryProviders(
47+
fn (ProviderInterface $provider) => $provider->chat($messages, $options)
48+
);
49+
}
50+
51+
public function stream(array $messages, array $options = []): iterable
52+
{
53+
return $this->tryProviders(
54+
fn (ProviderInterface $provider) => $provider->stream($messages, $options)
55+
);
56+
}
57+
58+
public function embed(string|array $input, array $options = []): EmbeddingResponse
59+
{
60+
return $this->tryProviders(function (ProviderInterface $provider) use ($input, $options) {
61+
if (!$provider instanceof EmbeddingProviderInterface) {
62+
throw new RuntimeException("Provider {$provider->getName()} does not support embeddings");
63+
}
64+
65+
return $provider->embed($input, $options);
66+
});
67+
}
68+
69+
public function supportsTool(): bool
70+
{
71+
foreach ($this->providers as $provider) {
72+
if ($provider->supportsTool()) {
73+
return true;
74+
}
75+
}
76+
77+
return false;
78+
}
79+
80+
public function supportsVision(): bool
81+
{
82+
foreach ($this->providers as $provider) {
83+
if ($provider->supportsVision()) {
84+
return true;
85+
}
86+
}
87+
88+
return false;
89+
}
90+
91+
public function supportsStructuredOutput(): bool
92+
{
93+
foreach ($this->providers as $provider) {
94+
if ($provider->supportsStructuredOutput()) {
95+
return true;
96+
}
97+
}
98+
99+
return false;
100+
}
101+
102+
public function getName(): string
103+
{
104+
return 'failover';
105+
}
106+
107+
/**
108+
* Get the provider that was last used successfully.
109+
*/
110+
public function getLastUsedProvider(): ?ProviderInterface
111+
{
112+
return $this->lastUsedProvider;
113+
}
114+
115+
/**
116+
* Try each provider in order, failing over on exception.
117+
*
118+
* @template T
119+
* @param callable(ProviderInterface): T $operation
120+
* @return T
121+
*/
122+
private function tryProviders(callable $operation): mixed
123+
{
124+
$lastException = null;
125+
126+
foreach ($this->providers as $provider) {
127+
try {
128+
$result = $operation($provider);
129+
$this->lastUsedProvider = $provider;
130+
131+
return $result;
132+
} catch (Throwable $e) {
133+
if (!$this->shouldRetry($e)) {
134+
throw $e;
135+
}
136+
$lastException = $e;
137+
}
138+
}
139+
140+
throw new RuntimeException(
141+
'All providers failed. Last error: ' . ($lastException?->getMessage() ?? 'unknown'),
142+
0,
143+
$lastException,
144+
);
145+
}
146+
147+
/**
148+
* Check if an exception should trigger failover.
149+
*/
150+
private function shouldRetry(Throwable $e): bool
151+
{
152+
if (empty($this->retryOn)) {
153+
return true;
154+
}
155+
156+
foreach ($this->retryOn as $exceptionClass) {
157+
if ($e instanceof $exceptionClass) {
158+
return true;
159+
}
160+
}
161+
162+
return false;
163+
}
164+
}

src/SearchResult.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of PapiAI,
5+
* A simple but powerful PHP library for building AI agents.
6+
*
7+
* (c) Marcello Duarte <marcello.duarte@gmail.com>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
declare(strict_types=1);
14+
15+
namespace PapiAI\Core;
16+
17+
final class SearchResult
18+
{
19+
/**
20+
* @param string $id The document identifier
21+
* @param float $score Similarity score (0.0 to 1.0, higher = more similar)
22+
* @param array<string, mixed> $metadata Associated metadata
23+
* @param string|null $content Original text content, if stored
24+
*/
25+
public function __construct(
26+
public readonly string $id,
27+
public readonly float $score,
28+
public readonly array $metadata = [],
29+
public readonly ?string $content = null,
30+
) {
31+
}
32+
}

0 commit comments

Comments
 (0)