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 $documentsint $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.0mb_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 $userRolestring $chunkAccess, array $roleToScopes): bool {
    
$scopes $roleToScopes[$userRole] ?? ['all'];

    return 
$chunkAccess === 'all' || in_array($chunkAccess$scopestrue);
}

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