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'],
];