Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ venv/
ENV/
.env

# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# IDEs
.vscode/
.idea/
Expand All @@ -17,5 +23,6 @@ ENV/

# TWELVE Data
backend/data/
backend/cloned_repos/
backend/evaluation/results/
backend/tests/validation_report.html
5 changes: 5 additions & 0 deletions backend/src/agents/main_agent/integration/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# integration package — ORACLE → MAIN adapter layer
from .oracle_adapter import OracleAdapter
from .oracle_schema import NormalizedOracleOutput

__all__ = ["OracleAdapter", "NormalizedOracleOutput"]
92 changes: 92 additions & 0 deletions backend/src/agents/main_agent/integration/compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
compatibility.py
────────────────
Schema compatibility layer between ORACLE output versions and the adapter.

When ORACLE adds, renames, or restructures a field, this file alone is updated.
MAIN Agent and oracle_adapter.py must remain untouched for schema drift.
"""

from __future__ import annotations

import logging
from typing import Any, Dict, List

logger = logging.getLogger(__name__)


def _shim_legacy_viva_field(payload: Dict[str, Any]) -> None:
"""Ensure both viva target list keys are present."""
if "viva_intelligence_targets" not in payload:
payload["viva_intelligence_targets"] = []
if "implementation_viva_targets" not in payload:
payload["implementation_viva_targets"] = []


def _shim_project_graph_to_execution_graph(payload: Dict[str, Any]) -> None:
"""Map old 'project_graph' key to the newer 'execution_graph' shape."""
if "execution_graph" not in payload and "project_graph" in payload:
pg = payload["project_graph"]
payload["execution_graph"] = {
"nodes": pg.get("nodes", []),
"edges": pg.get("edges", []),
"middleware": [],
"db_calls": [],
"auth_points": [],
"risk_flags": [],
"failure_paths": [],
}
logger.debug("[compatibility] Shimmed 'project_graph' → 'execution_graph'")


def _shim_missing_evidence_fields(payload: Dict[str, Any]) -> None:
"""Inject empty evidence lists on EvidenceModel dicts that omit them."""
for key in [
"project_name", "project_type", "backend_framework",
"frontend_framework", "database_used", "authentication_system",
"architecture_pattern",
]:
field = payload.get(key)
if isinstance(field, dict) and "evidence" not in field:
field["evidence"] = []


def _shim_flat_string_failure_paths(payload: Dict[str, Any]) -> None:
"""Coerce List[str] failure_paths to List[EvidenceModel dict]."""
raw = payload.get("failure_paths", [])
if not isinstance(raw, list):
return
payload["failure_paths"] = [
{"value": item, "confidence": 0.7, "evidence": []}
if isinstance(item, str) else item
for item in raw
]


def _shim_runtime_risks_severity(payload: Dict[str, Any]) -> None:
"""Uppercase severity in runtime_risks for enum coercion consistency."""
risks = payload.get("runtime_risks", [])
if not isinstance(risks, list):
return
for risk in risks:
if isinstance(risk, dict) and "severity" in risk:
risk["severity"] = str(risk["severity"]).upper()


_SHIMS = [
_shim_legacy_viva_field,
_shim_project_graph_to_execution_graph,
_shim_missing_evidence_fields,
_shim_flat_string_failure_paths,
_shim_runtime_risks_severity,
]


def apply_compatibility_shims(payload: Dict[str, Any]) -> Dict[str, Any]:
"""Apply all registered compatibility shims. Mutates in-place, returns same dict."""
for shim in _SHIMS:
try:
shim(payload)
except Exception as exc:
logger.warning("[compatibility] Shim %s raised %s — skipping.", shim.__name__, exc)
return payload
109 changes: 109 additions & 0 deletions backend/src/agents/main_agent/integration/oracle_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""
oracle_adapter.py
─────────────────
Public entry point — the ONLY class MAIN Agent should import from this package.

Usage
─────
from src.agents.main_agent.integration import OracleAdapter, NormalizedOracleOutput

# From a live StructuredContext object
output: NormalizedOracleOutput = OracleAdapter.from_context(context)

# From a raw dict (e.g. WebSocket payload)
output: NormalizedOracleOutput = OracleAdapter.from_dict(raw_dict)

Contract
────────
- Never raises on malformed input; returns a safe fallback NormalizedOracleOutput.
- All validation errors are captured in output.adapter_warnings.
- MAIN must never import StructuredContext or any ORACLE-internal model.
"""

from __future__ import annotations

import copy
import logging
from typing import Any, Dict

from src.models.context import StructuredContext

from .compatibility import apply_compatibility_shims
from .oracle_schema import NormalizedOracleOutput
from .payload_normalizer import normalize_payload
from .validation import OraclePayloadValidationError, validate_raw_payload

logger = logging.getLogger(__name__)


class OracleAdapter:
"""
Converts ORACLE output (StructuredContext or raw dict) into
a stable NormalizedOracleOutput for MAIN Agent consumption.

All methods are classmethods — no instantiation needed.
"""

# ── Primary entry points ──────────────────────────────────────────────────

@classmethod
def from_context(cls, context: StructuredContext) -> NormalizedOracleOutput:
"""
Convert a live StructuredContext object into NormalizedOracleOutput.

Serialises the context to dict first so the rest of the pipeline
stays fully decoupled from ORACLE internal types.
"""
try:
raw = context.model_dump()
except AttributeError:
raw = context.dict() # pydantic v1 fallback
return cls.from_dict(raw)

@classmethod
def from_dict(cls, raw: Any) -> NormalizedOracleOutput:
"""
Convert a raw ORACLE payload dict into NormalizedOracleOutput.

Never raises — all failures produce a safe fallback with populated
adapter_warnings so MAIN can log and handle degraded state.
"""
if not isinstance(raw, dict):
logger.error("[OracleAdapter] Received non-dict payload: %s", type(raw).__name__)
return cls._safe_fallback(
[f"Payload must be a dict, got {type(raw).__name__!r}"]
)

# Work on a deep copy so we never mutate the caller's object.
payload: Dict[str, Any] = copy.deepcopy(raw)

# 1. Apply compatibility shims (handles schema evolution silently).
apply_compatibility_shims(payload)

# 2. Validate — collect warnings, reject fatally malformed payloads.
warnings: list[str] = []
try:
_, warnings = validate_raw_payload(payload)
except (OraclePayloadValidationError, TypeError) as exc:
logger.warning("[OracleAdapter] Validation failed: %s", exc)
return cls._safe_fallback([str(exc)])

if warnings:
for w in warnings:
logger.debug("[OracleAdapter] Warning: %s", w)

# 3. Normalize.
try:
return normalize_payload(payload, warnings)
except Exception as exc: # noqa: BLE001
logger.error("[OracleAdapter] Normalization error: %s", exc, exc_info=True)
return cls._safe_fallback(
[f"Normalization failed: {exc}"] + warnings
)

# ── Internal helpers ──────────────────────────────────────────────────────

@staticmethod
def _safe_fallback(warnings: list[str]) -> NormalizedOracleOutput:
"""Return a fully-initialized, zeroed NormalizedOracleOutput with warnings."""
return NormalizedOracleOutput(adapter_warnings=warnings)
131 changes: 131 additions & 0 deletions backend/src/agents/main_agent/integration/oracle_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
oracle_schema.py
────────────────
Stable, versioned data contracts that MAIN Agent consumes.

These models are the *only* shapes MAIN should ever reference.
They must never import StructuredContext or any ORACLE-internal model directly.
Adapter-layer (oracle_adapter.py) is responsible for populating them.
"""

from __future__ import annotations

from enum import Enum
from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field


# ── Schema version ───────────────────────────────────────────────────────────

ADAPTER_SCHEMA_VERSION = "1.0.0"


# ── Enumerations ─────────────────────────────────────────────────────────────

class VivaCategory(str, Enum):
ARCHITECTURE = "Architecture"
TRADEOFF = "Tradeoff"
SECURITY = "Security"
SCALABILITY = "Scalability"
FAILURE_PATH = "Failure-Path"
RUNTIME = "Runtime"


class DifficultyLevel(str, Enum):
EASY = "easy"
MEDIUM = "medium"
HARD = "hard"


class SeverityLevel(str, Enum):
LOW = "LOW"
MEDIUM = "MEDIUM"
HIGH = "HIGH"
CRITICAL = "CRITICAL"


# ── Atomic building blocks ────────────────────────────────────────────────────

class EvidenceLink(BaseModel):
"""A single piece of traceable evidence backing a claim."""
text: str
confidence: float = Field(ge=0.0, le=1.0)


class ObservableSignal(BaseModel):
"""
A normalized, observable property of the analysed project.
MAIN uses these to drive orchestration decisions (e.g. question difficulty,
persona tone, session length).
"""
key: str # e.g. "backend_framework", "auth_system"
value: str # e.g. "FastAPI", "JWT"
confidence: float = Field(ge=0.0, le=1.0)
evidence: List[EvidenceLink] = []


class NormalizedVivaTarget(BaseModel):
"""A single viva question target ready for MAIN orchestration."""
topic: str
question_target: str
difficulty: DifficultyLevel
category: VivaCategory
importance_score: float = Field(ge=0.0, le=1.0)
depth_score: float = Field(ge=0.0, le=10.0)
focus: str
related_node: str = ""
confidence: float = Field(ge=0.0, le=1.0)
reasoning_summary: str = ""
evidence: List[EvidenceLink] = []


class FailureScenario(BaseModel):
"""A single normalized failure path or risk flag from ORACLE."""
description: str
severity: SeverityLevel
confidence: float = Field(ge=0.0, le=1.0)
evidence: List[EvidenceLink] = []


class InconsistencySignal(BaseModel):
"""A doc-vs-code inconsistency detected by ORACLE."""
issue: str
severity: SeverityLevel
confidence: float = Field(ge=0.0, le=1.0)
evidence: List[EvidenceLink] = []


# ── Top-level normalized output ───────────────────────────────────────────────

class NormalizedOracleOutput(BaseModel):
"""
The single, stable output contract between ORACLE and MAIN Agent.

MAIN must only ever read fields defined here.
No internal ORACLE types should leak past this boundary.
"""
schema_version: str = ADAPTER_SCHEMA_VERSION

# Project identity
project_name: str = "Unknown"
project_type: str = "Unknown"
architecture_pattern: str = "Unknown"

# Core intelligence surfaces
observable_signals: List[ObservableSignal] = []
viva_targets: List[NormalizedVivaTarget] = []
failure_scenarios: List[FailureScenario] = []
inconsistencies: List[InconsistencySignal] = []

# Execution graph summary (MAIN never touches graph internals)
execution_node_count: int = 0
execution_edge_count: int = 0
middleware_count: int = 0
auth_point_count: int = 0

# Complexity signal
complexity_mismatch_detected: bool = False
complexity_mismatch_note: str = ""

# Adapter metadata
adapter_warnings: List[str] = []
Loading
Loading