Festi AI
A PHP library providing a unified interface for multiple AI providers: OpenAI, Claude (Anthropic), and Ollama (local models).
Requirements
- PHP >= 8.0
- ext-curl
Installation
composer require festi-team/festi-framework-ai
TODO
- [ ] Add support Function Calling
Usage
All services implement the IAIConnection interface with three methods: connect(), ask(IPrompt $prompt), and disconnect().
OpenAI
use AI\Config\AIConfig;
use AI\Services\OpenAIService;
use AI\Services\OpenAI\OpenAIClient;
use AI\Prompt\Prompt;
$prompt = (new Prompt())
->addCondition("What is the capital of France?")
->addCondition("Please provide a brief explanation.");
$config = new AIConfig(
model: 'gpt-4o',
apiKey: 'sk-proj-XXX',
orgId: 'org-XXX'
);
$connector = new OpenAIService($config, new OpenAIClient($config));
$connector->connect();
$response = $connector->ask($prompt);
echo $response->getAnswer()->getContent();
Claude (Anthropic)
use AI\Config\AIConfig;
use AI\Services\ClaudeService;
use AI\Services\Claude\ClaudeClient;
use AI\Prompt\Prompt;
$prompt = (new Prompt())
->addCondition("What is the capital of France?")
->addCondition("Please provide a brief explanation.");
$config = new AIConfig(
model: 'claude-sonnet-4-20250514',
apiKey: 'sk-ant-XXX',
endpoint: 'https://api.anthropic.com/v1/',
maxTokens: 2048 // Claude requires max_tokens (default: 1024)
);
$connector = new ClaudeService($config, new ClaudeClient($config));
$connector->connect();
$response = $connector->ask($prompt);
echo $response->getAnswer()->getContent();
Notes:
- Claude requires the max_tokens parameter. Set it via AIConfig or it defaults to 1024.
- The role set on the Prompt is sent as Claude's top-level system parameter.
Ollama (Local Models)
use AI\Config\AIConfig;
use AI\Services\OllamaService;
use AI\Services\Ollama\OllamaClient;
use AI\Prompt\Prompt;
$prompt = (new Prompt())
->addCondition("What is the capital of France?")
->addCondition("Please provide a brief explanation.");
$config = new AIConfig(
model: 'phi3:latest',
apiKey: null,
endpoint: 'http://localhost:11434/v1/'
);
$connector = new OllamaService($config, new OllamaClient($config));
$connector->connect();
$response = $connector->ask($prompt);
echo $response->getAnswer()->getContent();
AIConfig Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
model |
string |
(required) | Model ID (e.g. gpt-4o, claude-sonnet-4-20250514) |
apiKey |
?string |
null |
API key (not needed for local Ollama) |
orgId |
?string |
null |
Organization ID (OpenAI only) |
endpoint |
?string |
https://api.openai.com/v1/ |
API base URL |
timeout |
int |
30 |
HTTP request timeout in seconds |
maxTokens |
?int |
null |
Max tokens for response (required for Claude) |
Load and save a prompt to a JSON file
$prompt = new Prompt();
$prompt->addCondition("Fix the following code style issues:")
->addCondition("- Do not change business logic.")
->setTemplate("Here are the conditions:\n{conditions}\n{snippets}")
->setRole("Developer")
->setResultFormat("Code output only");
// Save to file
$prompt->saveToFile('prompt.json');
// Load from file
$loadedPrompt = Prompt::loadFromFile('prompt.json');
echo $loadedPrompt;
Image Recognition with AI
Example of using AI to extract data from an image.
class BillProcessor
{
private string $imagesDir;
private string $processedDir;
private string $dataDir;
private OpenAIService $aiService;
private ImageManager $imageManager;
private Writer $csvWriter;
public function __construct(
private string $projectRoot,
private array $config
) {
$this->imagesDir = $projectRoot . 'bills/images/';
$this->processedDir = $projectRoot . 'bills/processed/';
$this->dataDir = $projectRoot . 'bills/data/';
$this->initializeServices();
}
private function initializeServices(): void
{
// Initialize AI Service
$aiConfig = new AIConfig(
$this->config['ai']['model_id'] ?? 'gpt-4-vision-preview',
$this->config['ai']['api_key'],
$this->config['ai']['org_id']
);
$this->aiService = new OpenAIService($aiConfig, new OpenAIClient($aiConfig));
$this->aiService->connect();
// Initialize Image Manager
$this->imageManager = new ImageManager(['driver' => 'gd']);
// Initialize CSV Writer
$this->csvWriter = Writer::createFromPath($this->dataDir . 'bills.csv', 'a+');
$this->csvWriter->insertOne(['id', 'date', 'amount', 'category', 'image_path', 'processed_date']);
}
public function processBill(string $imagePath): array
{
// Generate unique ID
$id = uniqid('bill_');
// Process image
$image = $this->imageManager->make($imagePath);
$processedPath = $this->processedDir . $id . '.jpg';
$image->save($processedPath);
// Extract data using AI
$extractedData = $this->extractDataFromImage($imagePath);
// Save to CSV
$this->saveToCsv([
'id' => $id,
'date' => $extractedData['date'],
'amount' => $extractedData['amount'],
'category' => $extractedData['category'],
'image_path' => $processedPath,
'processed_date' => date('Y-m-d H:i:s')
]);
return $extractedData;
}
private function extractDataFromImage(string $imagePath): array
{
$prompt = (new Prompt())
->addCondition("Extract the following information from this bill image:")
->addCondition("1. Date of the bill")
->addCondition("2. Total amount")
->addCondition("3. Category (from the following list: " . implode(', ', $GLOBALS['categories']) . ")")
->setResultFormat("Return the data in JSON format with keys: date, amount, category")
->addFile($imagePath);
$response = $this->aiService->ask($prompt);
return json_decode($response->getAnswer()->getContent(), true);
}
public function processAllBills(): array
{
$results = [];
$files = glob($this->imagesDir . '*.{jpg,jpeg,png}', GLOB_BRACE);
foreach ($files as $file) {
try {
$result = $this->processBill($file);
$results[] = [
'file' => basename($file),
'status' => 'success',
'data' => $result
];
} catch (\Exception $e) {
$results[] = [
'file' => basename($file),
'status' => 'error',
'error' => $e->getMessage()
];
}
}
return $results;
}
}
Usage:
// Initialize processor
$processor = new BillProcessor(
PROJECT_ROOT,
[
'ai' => [
'model_id' => 'gpt-4-vision-preview',
'api_key' => $GLOBALS['config']['ai']['api_key'],
'org_id' => $GLOBALS['config']['ai']['org_id']
]
]
);
// Process a single bill
$result = $processor->processBill('bills/images/example_bill.jpg');
// Process all bills in directory
$results = $processor->processAllBills();
Document File Processing
Both OpenAIClient and ClaudeClient support processing various document file types through an extensible extractor system. By default, text-based files (TXT, CSV, JSON, XML, Markdown, etc.) are automatically supported. For PDF files, you can register optional extractors.
Default Extractors
The DocumentTextExtractorRegistry automatically registers two extractors:
1. SimpleTextExtractor — supports common text file formats:
- Extensions:
txt,csv,json,xml,md,markdown,log,ini,conf,yaml,yml - MIME Types:
text/plain,text/csv,application/json,text/xml,application/xml,text/markdown,text/x-log
Encoding: Content is normalized to UTF-8. If the file is not valid UTF-8, encoding is detected from UTF-8, ISO-8859-1, Windows-1252, and ASCII (fallback: ISO-8859-1), then converted with iconv(…, 'UTF-8//IGNORE', …). Invalid characters are dropped. If conversion fails, AIServiceException is thrown.
2. PdfExtractor — supports PDF files (requires smalot/pdfparser):
- Extensions:
pdf - Extracts text from PDF pages. If extraction returns empty (e.g. complex styled PDFs), the file is sent as base64 for the API to read server-side (OpenAI) or as plain text fallback (Claude).
Both extractors work immediately without any configuration:
use AI\Config\AIConfig;
use AI\Services\OpenAIService;
use AI\Services\OpenAI\OpenAIClient;
use AI\Prompt\Prompt;
$config = new AIConfig($modelId, $apiKey, $orgId);
$client = new OpenAIClient($config);
$service = new OpenAIService($config, $client);
$service->connect();
// Process a text file - works automatically!
$prompt = (new Prompt())
->addCondition("Summarize the content of this file:")
->addFile('document.txt'); // or .csv, .json, .xml, .md, etc.
$response = $service->ask($prompt);
echo $response->getAnswer()->getContent();
With ClaudeClient:
use AI\Config\AIConfig;
use AI\Services\ClaudeService;
use AI\Services\Claude\ClaudeClient;
use AI\Prompt\Prompt;
$config = new AIConfig(
model: 'claude-sonnet-4-20250514',
apiKey: 'sk-ant-XXX',
endpoint: 'https://api.anthropic.com/v1/',
maxTokens: 2048
);
$client = new ClaudeClient($config);
$service = new ClaudeService($config, $client);
$service->connect();
// Process a text file - works automatically!
$prompt = (new Prompt())
->addCondition("Summarize the content of this file:")
->addFile('document.txt');
$response = $service->ask($prompt);
echo $response->getAnswer()->getContent();
Claude handles images as image content parts and extracted text as text content parts. Files that are not images and have no registered extractor are sent as plain text if they are valid UTF-8.
PDF Support
PDF extraction is registered by default via PdfExtractor. It requires the smalot/pdfparser package:
composer require smalot/pdfparser
Once installed, PDF files work automatically:
$prompt = (new Prompt())
->addCondition("Extract key points from this document:")
->addFile('report.pdf');
$response = $service->ask($prompt);
Mixing Images and Documents
You can mix images and document files in the same request:
$prompt = (new Prompt())
->addCondition("Compare the information in the image with the document:")
->addFile('screenshot.png') // Image file
->addFile('report.pdf'); // Document file
$response = $service->ask($prompt);
Advanced Usage
Create custom extractors:
Implement the IDocumentExtractor interface:
use AI\Utils\Extractor\IDocumentExtractor;
class CustomExtractor implements IDocumentExtractor
{
public function getFileContent(string $filePath, string $mimeType, string $extension): string
{
// Your extraction logic here
return $extractedText;
}
public function isSupportFileType(string $mimeType, string $extension): bool
{
return $extension === 'custom' || $mimeType === 'application/custom';
}
}
$client->registerDocumentExtractor(new CustomExtractor());
Custom extractors with new extensions:
Custom extractors work with any file extension. If a registered extractor supports the file type (by MIME/extension), its extracted text is sent as a "text" part. If no extractor matches and the file is a PDF, it is sent as a "file" part (base64) so the OpenAI API reads it server-side. For other unsupported file types, an AIServiceException is thrown.
Extractor priority:
Extractors are checked in registration order. The first extractor that returns true from isSupportFileType() will be used. Register more specific extractors before general ones.
Unsupported file types
For file types without a registered extractor:
- OpenAI: PDF files are sent as raw bytes (base64-encoded) with type: "file" so the OpenAI API reads them server-side. Other unsupported types throw AIServiceException.
- Claude: Files that are valid UTF-8 are sent as text content. Binary files throw AIServiceException.
Error handling
Errors are thrown when: - The file cannot be read - A registered extractor throws during extraction - No extractor is registered and the file type is not supported for base64 fallback (only PDF is supported) - An extractor returns empty text and the file type is not supported for base64 fallback
Supported file types
Images (sent as base64 image_url):
- jpeg, jpg, png, gif, webp, bmp
Documents with default extractors (text extracted in PHP and sent as "text" part):
- SimpleTextExtractor: txt, csv, json, xml, md, markdown, log, ini, conf, yaml, yml
- PdfExtractor: pdf (requires smalot/pdfparser). If text extraction returns empty (e.g. complex styled PDFs), the file is sent as base64 type: "file" for OpenAI to read server-side.
PDF files with no extractor:
- Sent as type: "file" (base64); the OpenAI API reads them server-side.
Other documents with no extractor:
- An AIServiceException is thrown. Register a custom extractor to support additional file types.
OpenAIClient chat completions
When calling the OpenAI API directly via OpenAIClient, use chatCompletions():
use AI\Services\OpenAI\Request\ChatCompletionRequest;
$client = new OpenAIClient($config);
$request = new ChatCompletionRequest(
$messages, // ChatMessage[]
$model, // string, e.g. 'gpt-4o'
$files, // array of file paths (optional, default [])
$maxTokens // int|null optional; max tokens for the completion; null = provider default
);
$response = $client->chatCompletions($request);
- messages: Array of
AI\Services\OpenAI\Message\ChatMessageinstances (e.g. system, user). - model: Model (e.g.
gpt-4o,gpt-3.5-turbo). - files: Optional array of file paths; content is prepared via registered extractors or sent as base64.
- maxTokens: Optional. Caps the length of the model response; reduces cost and latency. Omit or pass
nullto use the provider default.
ClaudeClient Messages API
When calling the Claude API directly via ClaudeClient, use sendMessage():
use AI\Services\Claude\ClaudeClient;
use AI\Services\Claude\Message\ClaudeMessage;
use AI\Services\Claude\Request\ClaudeMessageRequest;
$client = new ClaudeClient($config);
$request = new ClaudeMessageRequest(
$messages, // ClaudeMessage[]
$model, // string, e.g. 'claude-sonnet-4-20250514'
$system, // string|null, system prompt (optional)
$maxTokens, // int, required (default 1024)
$files // array of file paths (optional, default [])
);
$response = $client->sendMessage($request);
- messages: Array of
AI\Services\Claude\Message\ClaudeMessageinstances (role:userorassistant). - model: Claude model ID (e.g.
claude-sonnet-4-20250514,claude-haiku-4-5-20251001). - system: Optional system prompt, sent as a top-level
systemfield in the Claude API. - maxTokens: Required by Claude. Default is
1024. - files: Optional array of file paths; processed via registered extractors or sent as base64.
Ollama
ollama list
ollama restart
ollama run phi3
curl http://localhost:11434/v1/models
curl -X POST http://localhost:11434/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "phi3:latest",
"prompt": "What is the capital of France?"
}'
Contributing
Contributions are welcome! Please read ./CONTRIBUTING.md for guidelines on how to submit bug reports, feature requests, and pull requests.
./LICENSE
This library is released under the MIT ./LICENSE. Copyright (c) 2026 VARTEQ, Inc.