Case 1. Internal corporate assistant for regulations and policies
A pure PHP RAG example for answering employee questions from approved internal documents
In this case, we build a simple corporate knowledge assistant with chunking, embeddings, retrieval, access filtering, and controlled context building before answer generation.
Example of code:
<?php
$documents = [
[
'id' => 1,
'document' => 'HR Policy v3',
'section' => '4.2',
'access' => 'all',
'content' => 'Remote work from another country is possible only after approval from HR and the direct manager. The request must be submitted before travel and include the planned location and period.',
],
[
'id' => 2,
'document' => 'Security Policy v2',
'section' => '2.1',
'access' => 'all',
'content' => 'Security incidents must be reported within 24 hours after discovery. The report must include incident type, affected systems, and initial mitigation steps.',
],
[
'id' => 3,
'document' => 'Legal Incident Playbook',
'section' => '7.3',
'access' => 'legal',
'content' => 'Notification to external legal parties must be prepared within 72 hours after data leakage. All communication drafts require legal department approval before sending.',
],
];
// Splits source documents into sentence-based chunks while preserving metadata.
function splitIntoChunks(array $documents, int $sentencesPerChunk = 1): array {
$chunks = [];
$nextChunkId = 1;
foreach ($documents as $document) {
$sentences = preg_split('/(?<=[.!?])\s+/u', trim((string)$document['content'])) ?: [];
$buffer = [];
foreach ($sentences as $sentence) {
$sentence = trim($sentence);
if ($sentence === '') {
continue;
}
$buffer[] = $sentence;
if (count($buffer) === $sentencesPerChunk) {
$chunks[] = [
'id' => $nextChunkId++,
'document_id' => (int)$document['id'],
'document' => (string)$document['document'],
'section' => (string)$document['section'],
'access' => (string)$document['access'],
'content' => implode(' ', $buffer),
];
$buffer = [];
}
}
if ($buffer !== []) {
$chunks[] = [
'id' => $nextChunkId++,
'document_id' => (int)$document['id'],
'document' => (string)$document['document'],
'section' => (string)$document['section'],
'access' => (string)$document['access'],
'content' => implode(' ', $buffer),
];
}
}
return $chunks;
}
// Builds a simple lexical embedding vector for a text chunk.
// This is intentionally very simple and for demo purposes only.
function embedChunkText(string $text): array {
$normalized = strtolower($text);
$features = [
substr_count($normalized, 'remote'),
substr_count($normalized, 'country'),
substr_count($normalized, 'approval'),
substr_count($normalized, 'incident') + substr_count($normalized, 'leak'),
substr_count($normalized, 'report'),
substr_count($normalized, 'hour') + substr_count($normalized, 'day'),
];
$vector = [];
foreach ($features as $feature) {
$vector[] = (float)$feature + 0.001;
}
$vector[] = min(1.0, mb_strlen($normalized) / 200.0);
return $vector;
}
// Computes cosine similarity between two numeric vectors.
function cosineSimilarity(array $a, array $b): float {
$dot = 0.0;
$normA = 0.0;
$normB = 0.0;
foreach ($a as $i => $value) {
$dot += $value * $b[$i];
$normA += $value * $value;
$normB += $b[$i] * $b[$i];
}
if ($normA == 0.0 || $normB == 0.0) {
return 0.0;
}
return $dot / (sqrt($normA) * sqrt($normB));
}
// Checks whether the current user role can access a chunk by scope.
function canUserAccessChunk(string $userRole, string $chunkAccess, array $roleToScopes): bool {
$scopes = $roleToScopes[$userRole] ?? ['all'];
return $chunkAccess === 'all' || in_array($chunkAccess, $scopes, true);
}
// Produces a grounded answer using only the retrieved context chunks.
function buildAnswerFromContext(string $queryText, array $contextChunks): array {
if ($contextChunks === []) {
return [
'answer' => 'I cannot answer from approved documents available to your role.',
'source' => 'No accessible source found',
];
}
$joinedContext = strtolower(implode(' ', array_map(static fn(array $chunk): string => (string)$chunk['content'], $contextChunks)));
if (str_contains($joinedContext, '24 hour')) {
return [
'answer' => 'According to Security Policy v2, incidents must be reported within 24 hours after discovery.',
'source' => 'Security Policy v2, section 2.1',
];
}
if (str_contains($joinedContext, 'approval from hr')) {
return [
'answer' => 'According to HR Policy v3, remote work from another country requires prior approval from HR and the manager.',
'source' => 'HR Policy v3, section 4.2',
];
}
$topChunk = $contextChunks[0];
return [
'answer' => (string)$topChunk['content'],
'source' => (string)$topChunk['document'] . ', section ' . (string)$topChunk['section'],
];
}
$roleToScopes = [
'employee' => ['all', 'hr'],
'security-officer' => ['all', 'security'],
'legal' => ['all', 'legal'],
];