SIAA uses a sliding window chunking approach with overlap to divide documents into semantically coherent fragments. This prevents critical information (like multi-step procedures or legal articles) from being split across boundaries.
# Chunk parametersCHUNK_SIZE = 800 # Characters per chunkCHUNK_OVERLAP = 300 # Overlap between consecutive chunksMAX_CHUNKS_CONTEXTO = 3 # Maximum chunks to send per document
Each chunk remembers the last Markdown header seen before or within it:
siaa_proxy.py
def _ultimo_encabezado(texto: str) -> str: """Find the last Markdown heading in text.""" encabezados = re.findall(r'^#{1,3}\s+(.+)$', texto, re.MULTILINE) if encabezados: # Remove markdown formatting (*_`) and uppercase return re.sub(r'[*_`]', '', encabezados[-1]).strip().upper() return "INICIO"
Example:
## Artículo 5 — Responsabilidad de cargaLos funcionarios responsables de diligenciar el formulario SIERJU son:1. Jueces de conocimiento2. Magistrados de sala3. ...
Chunk metadata:
{ "texto": "Los funcionarios responsables de diligenciar...", "seccion": "ARTÍCULO 5 — RESPONSABILIDAD DE CARGA", "indice": 12}
This metadata appears in the context sent to Ollama:
[SEC: ARTÍCULO 5 — RESPONSABILIDAD DE CARGA | CHUNK: 12]Los funcionarios responsables de diligenciar el formulario SIERJU son:...
def puntuar_chunk(chunk: dict, palabras: set, pregunta_norm: str, terminos_prio: set, idf_local: dict = None) -> float: """ Score chunk relevance using multiple signals. Scoring system: +base×idf_local per keyword match (rare terms score higher) +15 if full question text appears in chunk +10 if chunk contains article with degree marker (art. 5°) +5 if chunk contains article without degree (artículo 5) +4 if chunk contains numbered list (procedures) +0-20 proximity bonus (keywords clustered within 150 chars) """ texto = chunk["texto"].lower() puntos = 0.0 # Keyword matching with TF-IDF for w in palabras: count = texto.count(w) if count > 0: tf = 1.0 + math.log(count) # Log-normalized: 1→1.0, 2→1.69, 5→2.61 base = 3.0 if w in terminos_prio else 1.0 if idf_local and w in idf_local: base *= idf_local[w] puntos += tf * base # Full question match (strongest signal) if pregunta_norm in texto: puntos += 15.0 # Article bonus if PATRON_ARTICULO_GRADO.search(chunk["texto"]): puntos += 10.0 # "artículo 5°", "art. 5º" elif PATRON_ARTICULO_SIMPLE.search(chunk["texto"]): puntos += 5.0 # "artículo 5" # Numbered list bonus (procedures) if re.search(r'^\s*\d+[\.\)]\s+\S', chunk["texto"], re.MULTILINE): puntos += 4.0 # Proximity bonus: keywords clustered in 150-char window if len(palabras) >= 2: VENTANA = 150 PASO = 50 max_densidad = 0.0 for i in range(0, max(1, len(texto) - VENTANA), PASO): v = texto[i:i+VENTANA] matches = sum(1 for w in palabras if w in v) if matches >= 2: d = matches / len(palabras) max_densidad = max(max_densidad, d) if max_densidad >= 0.90: puntos += 20.0 # 90%+ keywords together elif max_densidad >= 0.70: puntos += 12.0 elif max_densidad >= 0.50: puntos += 6.0 elif max_densidad >= 0.30: puntos += 2.0 return puntos
Local IDF Weighting
Problem: A term appearing in ALL chunks of a document provides no discriminative power.Solution: Calculate IDF within the document:
Francotirador mode: Top chunk scores 4.23x higher than second → very confident, send only best chunk Escopeta mode: Scores close together (ratio < 1.8) → uncertain, send 3 chunks to cover possibilities
for pts, idx, chunk in scored[:chunks_a_usar * 2]: if idx in indices_usados: continue texto = chunk["texto"] meta = f"[SEC: {chunk['seccion'][:60]} | CHUNK: {idx}]" seleccionados.append(meta + "\n" + texto) indices_usados.add(idx)nombre_display = os.path.splitext(doc["nombre_original"])[0].upper()etiqueta = f"[DOC: {nombre_display}]"separador = "\n" + "═" * 60 + "\n"return etiqueta + "\n" + "\n\n".join(seleccionados) + separador
Example output sent to Ollama:
[DOC: ACUERDO_NO._PSAA16-10476][SEC: ARTÍCULO 19 — VIGENCIA Y DEROGATORIAS | CHUNK: 35]Artículo 19. El incumplimiento de lo dispuesto en el presente acuerdo acarreará las sanciones disciplinarias establecidas en el Código Disciplinario Único para funcionarios judiciales...[SEC: ARTÍCULO 20 — SANCIONES ESPECÍFICAS | CHUNK: 36]Los funcionarios que no reporten la información dentro del plazo establecido (quinto día hábil) o la reporten de forma incompleta o inexacta podrán ser objeto de investigación...════════════════════════════════════════════════════════════