Files
a0_software_orchestrator/helpers/library/extractor.py
T
Software Orchestrator 5769c1cd22 Initial commit: a0_software_orchestrator v1.0
- Auto-Registration-Bug behoben (register_project/get_project_id/resolve_project Trennung)
- 25 Tests gruen (Pytest)
- block_compactor-Tool refactored (Option B: Soft-Check statt Hard-Block)
- 4 Restbaustellen gefixt
- DB-Schema: plugin_settings-Tabelle hinzugefuegt
- 3 Schattenprojekte aus DB geloescht
- Plan v3 + Refactor-Plan + Worklog dokumentiert
2026-06-16 22:13:06 +00:00

471 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
A0 Software Orchestrator Pattern-Extraktor
Extrahiert Patterns aus abgeschlossenen Projekt-Artefakten und speichert sie
in der Patterns-Datenbank.
Kontinuierliche Trigger:
- error_fixed: known_errors.md wurde aktualisiert
- task_completed: Task in task_graph.json als 'done' markiert
- deploy_ok: runtime_report.md zeigt Erfolg
- decision_made: decisions.md neuer Eintrag
- release_done: Release Audit abgeschlossen
"""
import json
import re
import hashlib
from pathlib import Path
from typing import Optional, List, Dict, Any
from datetime import datetime
from usr.plugins.a0_software_orchestrator.helpers.library.db import PatternDB, get_db
# ---------------------------------------------------------------------------
# Extraktor-Klasse
# ---------------------------------------------------------------------------
class PatternExtractor:
"""
Extrahiert Patterns aus Projekt-Artefakten.
Verwendung:
extractor = PatternExtractor(project_path)
patterns = extractor.extract_from_known_errors()
patterns += extractor.extract_from_decisions()
patterns += extractor.extract_from_architecture()
patterns += extractor.extract_from_docker()
patterns += extractor.extract_from_deployment()
"""
def __init__(self, project_path: Path, db: PatternDB = None):
self.project_path = Path(project_path)
self.a0_path = self.project_path / ".a0"
self.db = db or get_db()
self.project_name = self.project_path.name
self.project_id = None
# Projekt in Registry finden/registrieren
self._ensure_project_registered()
def _ensure_project_registered(self):
"""Stellt sicher, dass das Projekt in der Registry ist."""
existing = self.db.conn.execute(
"SELECT id FROM projects WHERE project_path = ? OR name = ?",
(str(self.project_path), self.project_name)
).fetchone()
if existing:
self.project_id = existing[0]
else:
# Tech-Stack aus Projekt-Artefakten erkennen
tech_stack = self._detect_tech_stack()
self.project_id = self.db.register_project(
name=self.project_name,
path=str(self.project_path),
tech_stack=tech_stack,
description=f"Automatisch registriert am {datetime.now().isoformat()}"
)
def _detect_tech_stack(self) -> Dict[str, Any]:
"""Erkennt den Tech-Stack aus Projekt-Dateien."""
stack = {}
# Python
if (self.project_path / "requirements.txt").exists():
content = (self.project_path / "requirements.txt").read_text()
if 'fastapi' in content.lower():
stack['backend'] = 'FastAPI'
elif 'flask' in content.lower():
stack['backend'] = 'Flask'
elif 'django' in content.lower():
stack['backend'] = 'Django'
if 'sqlalchemy' in content.lower():
stack['orm'] = 'SQLAlchemy'
if 'pydantic' in content.lower():
stack['validation'] = 'Pydantic'
if 'alembic' in content.lower():
stack['migrations'] = 'Alembic'
# Node/Frontend
if (self.project_path / "package.json").exists():
try:
pkg = json.loads((self.project_path / "package.json").read_text())
deps = {**pkg.get('dependencies', {}), **pkg.get('devDependencies', {})}
if 'react' in deps:
stack['frontend'] = 'React'
if 'next' in deps:
stack['frontend'] = 'Next.js'
if 'vue' in deps:
stack['frontend'] = 'Vue'
if 'typescript' in deps:
stack['language'] = 'TypeScript'
if 'vite' in deps:
stack['bundler'] = 'Vite'
except (json.JSONDecodeError, KeyError):
pass
# Docker
if (self.project_path / "Dockerfile").exists():
stack['docker'] = True
if (self.project_path / "docker-compose.yml").exists():
stack['docker_compose'] = True
# Datenbank aus env.md oder config
env_md = self.a0_path / "env.md"
if env_md.exists():
content = env_md.read_text().lower()
if 'postgresql' in content or 'postgres' in content:
stack['db'] = 'PostgreSQL'
elif 'mysql' in content:
stack['db'] = 'MySQL'
elif 'sqlite' in content:
stack['db'] = 'SQLite'
return stack
# -----------------------------------------------------------------------
# Extraktoren für verschiedene Artefakte
# -----------------------------------------------------------------------
def extract_from_known_errors(self) -> List[int]:
"""
Extrahiert error_solution-Patterns aus known_errors.md.
Nur Einträge, die eine Lösung enthalten (nicht nur Beschreibung).
"""
errors_file = self.a0_path / "known_errors.md"
if not errors_file.exists():
return []
content = errors_file.read_text(encoding='utf-8')
content_hash = hashlib.md5(content.encode()).hexdigest()
# Prüfen, ob diese Datei bereits extrahiert wurde
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(errors_file), content_hash)
).fetchone()
if already:
return [] # Keine Änderungen seit letzter Extraktion
pattern_ids = []
# Einfache Heuristik: Nach "## Error:" oder "### Lösung:" Blöcken suchen
# Block-Split an Doppel-Newlines
blocks = content.split('\n\n')
for block in blocks:
block = block.strip()
if not block or len(block) < 20:
continue
# Nur Blöcke mit Lösung extrahieren
has_solution = any(kw in block.lower() for kw in ['lösung', 'solution', 'fix', 'behoben', 'resolved'])
if not has_solution:
continue
# Titel aus erster Zeile
lines = block.split('\n')
title = lines[0].lstrip('#').strip()[:100]
if not title:
title = block[:80] + '...' if len(block) > 80 else block
# Pattern speichern
pid = self.db.add_pattern(
title=title,
category='error_fix',
pattern_type='error_solution',
description=block[:500],
code_example=self._extract_code_block(block),
source_project_id=self.project_id,
source_file=str(errors_file),
source_error=title,
validated=0 # Automatisch extrahiert → ungeprüft
)
# Embedding speichern (für Vektor-Suche)
self.db.store_embedding(pid, block[:1000])
# Lern-Log
self.db.log_learning(pid, self.project_id, 'error_fixed',
str(errors_file), content)
pattern_ids.append(pid)
return pattern_ids
def extract_from_decisions(self) -> List[int]:
"""
Extrahiert architecture_decision-Patterns aus decisions.md.
"""
decisions_file = self.a0_path / "decisions.md"
if not decisions_file.exists():
return []
content = decisions_file.read_text(encoding='utf-8')
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(decisions_file), content_hash)
).fetchone()
if already:
return []
pattern_ids = []
# ADR-Blöcke: ## ADR-001: Titel
adr_blocks = re.split(r'\n## ADR-\d+:', content)[1:] # Skip header
for i, block in enumerate(adr_blocks):
lines = block.strip().split('\n')
title = lines[0].strip() if lines else f"ADR-{i+1}"
# Entscheidung + Begründung extrahieren
decision = ""
rationale = ""
for line in lines:
if 'entscheidung' in line.lower() or 'decision' in line.lower():
decision = line.split(':', 1)[-1].strip() if ':' in line else line
if 'begründung' in line.lower() or 'rationale' in line.lower():
rationale = line.split(':', 1)[-1].strip() if ':' in line else line
description = f"Entscheidung: {decision}\nBegründung: {rationale}" if decision else block[:300]
pid = self.db.add_pattern(
title=title[:100],
category='architecture',
pattern_type='architecture_decision',
description=description,
why_it_works=rationale[:500] if rationale else None,
source_project_id=self.project_id,
source_file=str(decisions_file),
validated=0
)
self.db.store_embedding(pid, description)
self.db.log_learning(pid, self.project_id, 'decision_made',
str(decisions_file), content)
pattern_ids.append(pid)
return pattern_ids
def extract_from_architecture(self) -> List[int]:
"""
Extrahiert Patterns aus architecture.md.
Erkennt: Tech-Stack, Architekturmuster, Docker-Setup.
"""
arch_file = self.a0_path / "architecture.md"
if not arch_file.exists():
return []
content = arch_file.read_text(encoding='utf-8')
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(arch_file), content_hash)
).fetchone()
if already:
return []
pattern_ids = []
# Nach bekannten Architekturmustern suchen
patterns_to_detect = [
('FastAPI', 'python', 'fastapi', 'best_practice'),
('SQLAlchemy', 'database', 'sqlalchemy', 'best_practice'),
('React', 'frontend', 'react', 'best_practice'),
('Docker', 'docker', None, 'setup_guide'),
('JWT', 'security', None, 'best_practice'),
('REST API', 'architecture', None, 'architecture_decision'),
]
for tech, category, subcat, ptype in patterns_to_detect:
if tech.lower() in content.lower():
# Kontext um das Keyword extrahieren
idx = content.lower().find(tech.lower())
start = max(0, idx - 100)
end = min(len(content), idx + 300)
context = content[start:end].strip()
pid = self.db.add_pattern(
title=f"{tech} in {self.project_name}",
category=category,
subcategory=subcat,
pattern_type=ptype,
description=context,
source_project_id=self.project_id,
source_file=str(arch_file),
validated=0
)
self.db.store_embedding(pid, context)
self.db.log_learning(pid, self.project_id, 'release_audit',
str(arch_file), content)
pattern_ids.append(pid)
return pattern_ids
def extract_from_docker(self) -> List[int]:
"""
Extrahiert Docker-Patterns aus Dockerfile und docker-compose.yml.
"""
pattern_ids = []
dockerfile = self.project_path / "Dockerfile"
compose_file = self.project_path / "docker-compose.yml"
if dockerfile.exists():
content = dockerfile.read_text()
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(dockerfile), content_hash)
).fetchone()
if not already:
pid = self.db.add_pattern(
title=f"Dockerfile aus {self.project_name}",
category='docker',
pattern_type='setup_guide',
description=f"Docker-Konfiguration für {self.project_name}",
code_example=content,
source_project_id=self.project_id,
source_file=str(dockerfile),
validated=0
)
self.db.store_embedding(pid, content[:1000])
self.db.log_learning(pid, self.project_id, 'deployment_success',
str(dockerfile), content)
pattern_ids.append(pid)
if compose_file.exists():
content = compose_file.read_text()
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(compose_file), content_hash)
).fetchone()
if not already:
pid = self.db.add_pattern(
title=f"Docker Compose aus {self.project_name}",
category='docker',
pattern_type='deployment_config',
description=f"Docker-Compose-Konfiguration für {self.project_name}",
code_example=content,
source_project_id=self.project_id,
source_file=str(compose_file),
validated=0
)
self.db.store_embedding(pid, content[:1000])
self.db.log_learning(pid, self.project_id, 'deployment_success',
str(compose_file), content)
pattern_ids.append(pid)
return pattern_ids
def extract_from_deployment(self) -> List[int]:
"""
Extrahiert Deployment-Patterns aus runtime_report.md und env.md.
"""
pattern_ids = []
for filename in ['runtime_report.md', 'env.md']:
filepath = self.a0_path / filename
if not filepath.exists():
continue
content = filepath.read_text()
content_hash = hashlib.md5(content.encode()).hexdigest()
already = self.db.conn.execute(
"SELECT id FROM learn_log WHERE source_file = ? AND source_content_hash = ?",
(str(filepath), content_hash)
).fetchone()
if not already:
pid = self.db.add_pattern(
title=f"{filename.replace('.md','')} aus {self.project_name}",
category='deployment',
pattern_type='setup_guide',
description=content[:500],
source_project_id=self.project_id,
source_file=str(filepath),
validated=0
)
self.db.store_embedding(pid, content[:1000])
self.db.log_learning(pid, self.project_id, 'deployment_success',
str(filepath), content)
pattern_ids.append(pid)
return pattern_ids
def extract_all(self) -> Dict[str, List[int]]:
"""
Führt alle Extraktoren aus und gibt Summary zurück.
"""
results = {
'errors': self.extract_from_known_errors(),
'decisions': self.extract_from_decisions(),
'architecture': self.extract_from_architecture(),
'docker': self.extract_from_docker(),
'deployment': self.extract_from_deployment(),
}
# Projekt-Metriken updaten
total = sum(len(v) for v in results.values())
if total > 0:
self.db.conn.execute("""
UPDATE projects
SET patterns_extracted = COALESCE(patterns_extracted, 0) + ?,
last_extraction_at = datetime('now')
WHERE id = ?
""", (total, self.project_id))
self.db.conn.commit()
return results
@staticmethod
def _extract_code_block(text: str) -> Optional[str]:
"""Extrahiert einen Code-Block (```...```) aus Text."""
match = re.search(r'```[\s\S]*?```', text)
if match:
code = match.group(0)
# Markdown-Fences entfernen
code = re.sub(r'^```\w*\n?', '', code)
code = re.sub(r'\n?```$', '', code)
return code.strip()
return None
# ---------------------------------------------------------------------------
# Convenience-Funktion
# ---------------------------------------------------------------------------
def extract_from_project(project_path: str) -> Dict[str, Any]:
"""
Extrahiert Patterns aus einem Projekt und gibt eine Zusammenfassung zurück.
Args:
project_path: Pfad zum Projekt-Root
Returns:
Dict mit Summary der Extraktion
"""
extractor = PatternExtractor(Path(project_path))
results = extractor.extract_all()
total = sum(len(v) for v in results.values())
return {
'project': extractor.project_name,
'project_id': extractor.project_id,
'total_patterns_extracted': total,
'by_category': results,
'timestamp': datetime.now().isoformat()
}