From 4a13ec4fafd382e5ebde74b5fc6c6c1f9ca82b31 Mon Sep 17 00:00:00 2001 From: Aman koli <2025.amana@isu.ac.in> Date: Tue, 19 May 2026 20:15:42 +0530 Subject: [PATCH 1/2] feat: ORACLE Integration Adapter (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build normalized ORACLE integration adapter for the MAIN Agent. - oracle_schema.py: versioned NormalizedOracleOutput contract with ObservableSignal, NormalizedVivaTarget, FailureScenario, InconsistencySignal — no ORACLE internal types leak to MAIN - validation.py: strict schema validation, rejects malformed payloads with OraclePayloadValidationError, surfaces soft warnings - payload_normalizer.py: stateless normalization functions, merges both viva target lists, maps failure_paths + runtime_risks, preserves all evidence links intact - compatibility.py: shim pipeline for schema evolution (legacy project_graph key, flat string failure_paths, missing evidence lists, lowercase severity values) - oracle_adapter.py: single public entry point OracleAdapter with from_context() and from_dict() — never raises, always returns safe fallback with adapter_warnings on failure - tests/test_oracle_adapter.py: 30 tests across 6 categories: normalization, rejection, compatibility, evidence regression, determinism, isolation Closes #7 --- backend/src/agents/main_agent/__init__.py | 1 + .../agents/main_agent/integration/__init__.py | 5 + .../main_agent/integration/compatibility.py | 92 ++++ .../main_agent/integration/oracle_adapter.py | 109 +++++ .../main_agent/integration/oracle_schema.py | 131 ++++++ .../integration/payload_normalizer.py | 302 +++++++++++++ .../main_agent/integration/validation.py | 144 ++++++ backend/tests/test_oracle_adapter.py | 420 ++++++++++++++++++ 8 files changed, 1204 insertions(+) create mode 100644 backend/src/agents/main_agent/integration/__init__.py create mode 100644 backend/src/agents/main_agent/integration/compatibility.py create mode 100644 backend/src/agents/main_agent/integration/oracle_adapter.py create mode 100644 backend/src/agents/main_agent/integration/oracle_schema.py create mode 100644 backend/src/agents/main_agent/integration/payload_normalizer.py create mode 100644 backend/src/agents/main_agent/integration/validation.py create mode 100644 backend/tests/test_oracle_adapter.py diff --git a/backend/src/agents/main_agent/__init__.py b/backend/src/agents/main_agent/__init__.py index 5dc260f..5767396 100644 --- a/backend/src/agents/main_agent/__init__.py +++ b/backend/src/agents/main_agent/__init__.py @@ -1,3 +1,4 @@ +# main_agent package from .agent import MainAgent __all__ = ["MainAgent"] diff --git a/backend/src/agents/main_agent/integration/__init__.py b/backend/src/agents/main_agent/integration/__init__.py new file mode 100644 index 0000000..76f701f --- /dev/null +++ b/backend/src/agents/main_agent/integration/__init__.py @@ -0,0 +1,5 @@ +# integration package — ORACLE → MAIN adapter layer +from .oracle_adapter import OracleAdapter +from .oracle_schema import NormalizedOracleOutput + +__all__ = ["OracleAdapter", "NormalizedOracleOutput"] diff --git a/backend/src/agents/main_agent/integration/compatibility.py b/backend/src/agents/main_agent/integration/compatibility.py new file mode 100644 index 0000000..18cab79 --- /dev/null +++ b/backend/src/agents/main_agent/integration/compatibility.py @@ -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 diff --git a/backend/src/agents/main_agent/integration/oracle_adapter.py b/backend/src/agents/main_agent/integration/oracle_adapter.py new file mode 100644 index 0000000..b7b69b3 --- /dev/null +++ b/backend/src/agents/main_agent/integration/oracle_adapter.py @@ -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) diff --git a/backend/src/agents/main_agent/integration/oracle_schema.py b/backend/src/agents/main_agent/integration/oracle_schema.py new file mode 100644 index 0000000..489ef8f --- /dev/null +++ b/backend/src/agents/main_agent/integration/oracle_schema.py @@ -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] = [] diff --git a/backend/src/agents/main_agent/integration/payload_normalizer.py b/backend/src/agents/main_agent/integration/payload_normalizer.py new file mode 100644 index 0000000..25e4ca7 --- /dev/null +++ b/backend/src/agents/main_agent/integration/payload_normalizer.py @@ -0,0 +1,302 @@ +""" +payload_normalizer.py +───────────────────── +Stateless functions that convert a validated raw ORACLE payload dict +into the stable NormalizedOracleOutput schema. + +Rules +───── +- No ORACLE logic is re-implemented here (no AST, no confidence math). +- Every function is pure: same input → same output. +- Missing optional data yields safe defaults; no KeyError is ever raised. +- EvidenceLinks preserve the original evidence strings verbatim. +""" + +from __future__ import annotations + +from typing import Any, Dict, List + +from .oracle_schema import ( + DifficultyLevel, + EvidenceLink, + FailureScenario, + InconsistencySignal, + NormalizedOracleOutput, + NormalizedVivaTarget, + ObservableSignal, + SeverityLevel, + VivaCategory, +) +from .validation import validate_evidence_model + + +# ── Private helpers ─────────────────────────────────────────────────────────── + +def _evidence_links(raw_evidence: Any) -> List[EvidenceLink]: + """Convert a raw evidence list (strings or dicts) into EvidenceLink objects.""" + if not isinstance(raw_evidence, list): + return [] + links: List[EvidenceLink] = [] + for item in raw_evidence: + if isinstance(item, str) and item.strip(): + links.append(EvidenceLink(text=item.strip(), confidence=1.0)) + elif isinstance(item, dict): + text = str(item.get("text", item.get("value", ""))).strip() + confidence = float(item.get("confidence", 1.0)) + if text: + links.append(EvidenceLink(text=text, confidence=confidence)) + return links + + +def _safe_float(value: Any, default: float = 0.0, lo: float = 0.0, hi: float = 1.0) -> float: + """Clamp a numeric value to [lo, hi]; return default on failure.""" + try: + return max(lo, min(hi, float(value))) + except (TypeError, ValueError): + return default + + +def _safe_str(value: Any, default: str = "Unknown") -> str: + s = str(value).strip() if value is not None else "" + return s if s else default + + +def _coerce_severity(raw: Any) -> SeverityLevel: + mapping = { + "low": SeverityLevel.LOW, + "medium": SeverityLevel.MEDIUM, + "high": SeverityLevel.HIGH, + "critical": SeverityLevel.CRITICAL, + } + return mapping.get(str(raw).lower(), SeverityLevel.MEDIUM) + + +def _coerce_difficulty(raw: Any) -> DifficultyLevel: + mapping = { + "easy": DifficultyLevel.EASY, + "medium": DifficultyLevel.MEDIUM, + "hard": DifficultyLevel.HARD, + } + return mapping.get(str(raw).lower(), DifficultyLevel.MEDIUM) + + +def _coerce_category(raw: Any) -> VivaCategory: + mapping = { + "architecture": VivaCategory.ARCHITECTURE, + "tradeoff": VivaCategory.TRADEOFF, + "tradeoffs": VivaCategory.TRADEOFF, + "security": VivaCategory.SECURITY, + "scalability": VivaCategory.SCALABILITY, + "failure-path": VivaCategory.FAILURE_PATH, + "failure_path": VivaCategory.FAILURE_PATH, + "runtime": VivaCategory.RUNTIME, + } + return mapping.get(str(raw).lower(), VivaCategory.ARCHITECTURE) + + +# ── Observable signals ──────────────────────────────────────────────────────── + +_SIGNAL_KEYS = [ + "backend_framework", + "frontend_framework", + "database_used", + "authentication_system", +] + +def normalize_observable_signals(payload: Dict[str, Any]) -> List[ObservableSignal]: + """ + Extract the four core observable signals from the ORACLE payload. + Each maps to an EvidenceModel-shaped dict. + """ + signals: List[ObservableSignal] = [] + for key in _SIGNAL_KEYS: + raw = validate_evidence_model(payload.get(key, {}), key) + if raw["value"] and raw["value"].lower() != "unknown": + signals.append(ObservableSignal( + key=key, + value=_safe_str(raw["value"]), + confidence=_safe_float(raw["confidence"]), + evidence=_evidence_links(raw["evidence"]), + )) + return signals + + +# ── Viva targets ────────────────────────────────────────────────────────────── + +def normalize_viva_targets(payload: Dict[str, Any]) -> List[NormalizedVivaTarget]: + """ + Merge both viva target lists (legacy + current) and deduplicate. + Preserves every evidence link intact. + """ + raw_targets: List[Any] = [] + + legacy = payload.get("viva_intelligence_targets", []) + current = payload.get("implementation_viva_targets", []) + + if isinstance(legacy, list): + raw_targets.extend(legacy) + if isinstance(current, list): + raw_targets.extend(current) + + seen: set = set() + results: List[NormalizedVivaTarget] = [] + + for raw in raw_targets: + if not isinstance(raw, dict): + continue + question_target = _safe_str(raw.get("question_target"), "Unnamed Target") + topic = _safe_str(raw.get("topic"), "General") + dedup_key = (topic.lower(), question_target.lower()) + if dedup_key in seen: + continue + seen.add(dedup_key) + + results.append(NormalizedVivaTarget( + topic = topic, + question_target = question_target, + difficulty = _coerce_difficulty(raw.get("difficulty", "medium")), + category = _coerce_category(raw.get("category", "Architecture")), + importance_score = _safe_float(raw.get("importance_score", 0.5)), + depth_score = _safe_float(raw.get("depth_score", 5.0), lo=0.0, hi=10.0), + focus = _safe_str(raw.get("focus"), ""), + related_node = _safe_str(raw.get("related_node"), ""), + confidence = _safe_float(raw.get("confidence", 0.8)), + reasoning_summary= _safe_str(raw.get("reasoning_summary"), ""), + evidence = _evidence_links(raw.get("evidence", [])), + )) + + return results + + +# ── Failure scenarios ───────────────────────────────────────────────────────── + +def normalize_failure_scenarios(payload: Dict[str, Any]) -> List[FailureScenario]: + """ + Merge failure_paths, runtime_risks, and execution_graph.failure_paths + into a single flat list of FailureScenario objects. + """ + results: List[FailureScenario] = [] + + # From failure_paths (List[EvidenceModel]) + for raw in payload.get("failure_paths", []): + if not isinstance(raw, dict): + continue + desc = _safe_str(raw.get("value"), "") + if not desc: + continue + results.append(FailureScenario( + description = desc, + severity = SeverityLevel.MEDIUM, + confidence = _safe_float(raw.get("confidence", 0.7)), + evidence = _evidence_links(raw.get("evidence", [])), + )) + + # From runtime_risks (List[RuntimeRisk]) + for raw in payload.get("runtime_risks", []): + if not isinstance(raw, dict): + continue + desc = _safe_str(raw.get("value"), "") + if not desc: + continue + results.append(FailureScenario( + description = desc, + severity = _coerce_severity(raw.get("severity", "medium")), + confidence = _safe_float(raw.get("confidence", 0.7)), + evidence = _evidence_links(raw.get("evidence", [])), + )) + + # From execution_graph.risk_flags / failure_paths (plain strings) + eg = payload.get("execution_graph", {}) + if isinstance(eg, dict): + for label in eg.get("risk_flags", []): + results.append(FailureScenario( + description = _safe_str(label), + severity = SeverityLevel.MEDIUM, + confidence = 0.75, + evidence = [], + )) + for label in eg.get("failure_paths", []): + results.append(FailureScenario( + description = _safe_str(label), + severity = SeverityLevel.HIGH, + confidence = 0.80, + evidence = [], + )) + + return results + + +# ── Inconsistencies ─────────────────────────────────────────────────────────── + +def normalize_inconsistencies(payload: Dict[str, Any]) -> List[InconsistencySignal]: + results: List[InconsistencySignal] = [] + for raw in payload.get("inconsistencies", []): + if not isinstance(raw, dict): + continue + issue = _safe_str(raw.get("issue"), "") + if not issue: + continue + results.append(InconsistencySignal( + issue = issue, + severity = _coerce_severity(raw.get("severity", "medium")), + confidence = _safe_float(raw.get("confidence", 0.7)), + evidence = _evidence_links(raw.get("evidence", [])), + )) + return results + + +# ── Execution graph summary ─────────────────────────────────────────────────── + +def normalize_execution_graph_summary(payload: Dict[str, Any]) -> Dict[str, int]: + """Return a minimal int-only summary of the execution graph for MAIN.""" + eg = payload.get("execution_graph", {}) + if not isinstance(eg, dict): + return {"node_count": 0, "edge_count": 0, "middleware_count": 0, "auth_point_count": 0} + return { + "node_count": len(eg.get("nodes", [])), + "edge_count": len(eg.get("edges", [])), + "middleware_count":len(eg.get("middleware", [])), + "auth_point_count":len(eg.get("auth_points", [])), + } + + +# ── Full payload normalization ──────────────────────────────────────────────── + +def normalize_payload(validated_payload: Dict[str, Any], warnings: List[str] | None = None) -> NormalizedOracleOutput: + """ + Convert a *validated* raw ORACLE payload dict into NormalizedOracleOutput. + + This is the primary entry-point called by OracleAdapter. + `warnings` is the list returned by validate_raw_payload(). + """ + warnings = warnings or [] + + pn = validate_evidence_model(validated_payload.get("project_name", {}), "project_name") + pt = validate_evidence_model(validated_payload.get("project_type", {}), "project_type") + ap = validate_evidence_model(validated_payload.get("architecture_pattern", {}), "architecture_pattern") + + graph_summary = normalize_execution_graph_summary(validated_payload) + + cm = validated_payload.get("complexity_mismatch") + cm_detected = False + cm_note = "" + if isinstance(cm, dict): + cm_note = _safe_str(cm.get("value"), "") + cm_detected = bool(cm_note and "no major" not in cm_note.lower()) + + return NormalizedOracleOutput( + project_name = _safe_str(pn["value"]), + project_type = _safe_str(pt["value"]), + architecture_pattern = _safe_str(ap["value"]), + observable_signals = normalize_observable_signals(validated_payload), + viva_targets = normalize_viva_targets(validated_payload), + failure_scenarios = normalize_failure_scenarios(validated_payload), + inconsistencies = normalize_inconsistencies(validated_payload), + execution_node_count = graph_summary["node_count"], + execution_edge_count = graph_summary["edge_count"], + middleware_count = graph_summary["middleware_count"], + auth_point_count = graph_summary["auth_point_count"], + complexity_mismatch_detected = cm_detected, + complexity_mismatch_note = cm_note, + adapter_warnings = warnings, + ) diff --git a/backend/src/agents/main_agent/integration/validation.py b/backend/src/agents/main_agent/integration/validation.py new file mode 100644 index 0000000..31cd9cf --- /dev/null +++ b/backend/src/agents/main_agent/integration/validation.py @@ -0,0 +1,144 @@ +""" +validation.py +───────────── +Strict schema validation for raw ORACLE payloads. + +Responsibilities +──────────────── +- Validate that a raw payload dict contains the minimum expected keys. +- Reject clearly malformed payloads with an explicit ValidationError. +- Surface warnings for optional-but-expected fields that are missing. +- Never import StructuredContext directly; only inspect plain dicts. + +Rules +───── +- All public functions are pure (no side-effects). +- All failures are explicit — no silent coercion at this layer. +""" + +from __future__ import annotations + +from typing import Any, Dict, List, Tuple + + +# ── Required top-level keys ─────────────────────────────────────────────────── + +_REQUIRED_KEYS: List[str] = [ + "project_name", + "project_type", + "backend_framework", + "frontend_framework", + "database_used", + "authentication_system", + "architecture_pattern", +] + +# Keys that should be present and be lists; absence generates a warning, not an error. +_EXPECTED_LIST_KEYS: List[str] = [ + "viva_intelligence_targets", + "implementation_viva_targets", + "failure_paths", + "runtime_risks", + "inconsistencies", + "middleware_chain", + "security_flows", +] + + +# ── Public exceptions ───────────────────────────────────────────────────────── + +class OraclePayloadValidationError(ValueError): + """Raised when a raw ORACLE payload is structurally invalid.""" + + def __init__(self, message: str, missing_keys: List[str] | None = None): + super().__init__(message) + self.missing_keys = missing_keys or [] + + +# ── Core validation ─────────────────────────────────────────────────────────── + +def validate_raw_payload(payload: Any) -> Tuple[bool, List[str]]: + """ + Validate a raw ORACLE payload dict. + + Returns + ─────── + (is_valid: bool, warnings: List[str]) + + Raises + ────── + OraclePayloadValidationError — if the payload is fatally malformed. + TypeError — if the payload is not a dict at all. + """ + if not isinstance(payload, dict): + raise TypeError( + f"ORACLE payload must be a dict, got {type(payload).__name__!r}" + ) + + # ── Fatal checks ────────────────────────────────────────────────────────── + missing_required: List[str] = [ + key for key in _REQUIRED_KEYS if key not in payload + ] + if missing_required: + raise OraclePayloadValidationError( + f"ORACLE payload is missing required keys: {missing_required}", + missing_keys=missing_required, + ) + + # ── Structural checks on required EvidenceModel fields ──────────────────── + for key in _REQUIRED_KEYS: + field_val = payload[key] + if not isinstance(field_val, dict): + raise OraclePayloadValidationError( + f"Field '{key}' must be an EvidenceModel dict (got {type(field_val).__name__!r}). " + "Raw ORACLE output may have been pre-processed unexpectedly." + ) + if "value" not in field_val: + raise OraclePayloadValidationError( + f"EvidenceModel for '{key}' is missing the 'value' key." + ) + + # ── Soft checks (generate warnings only) ────────────────────────────────── + warnings: List[str] = [] + for key in _EXPECTED_LIST_KEYS: + if key not in payload: + warnings.append( + f"Optional list field '{key}' is absent from ORACLE payload. " + "Defaulting to empty list." + ) + elif not isinstance(payload[key], list): + warnings.append( + f"Field '{key}' is expected to be a list but got " + f"{type(payload[key]).__name__!r}. It will be treated as empty." + ) + + # ── execution_graph check ───────────────────────────────────────────────── + eg = payload.get("execution_graph") + if eg is None: + warnings.append("'execution_graph' is absent. Graph summary will be zeroed.") + elif not isinstance(eg, dict): + warnings.append( + f"'execution_graph' should be a dict, got {type(eg).__name__!r}. " + "Graph summary will be zeroed." + ) + + return True, warnings + + +def validate_evidence_model(raw: Any, field_name: str) -> Dict[str, Any]: + """ + Ensure a single EvidenceModel dict is sane. + + Returns the dict unchanged if valid, or a safe default if not. + Never raises — designed for use inside normalizers. + """ + if not isinstance(raw, dict): + return {"value": "Unknown", "confidence": 0.0, "evidence": []} + value = raw.get("value", "Unknown") + confidence = raw.get("confidence", 0.0) + evidence = raw.get("evidence", []) + if not isinstance(evidence, list): + evidence = [] + if not isinstance(confidence, (int, float)): + confidence = 0.0 + return {"value": str(value), "confidence": float(confidence), "evidence": evidence} diff --git a/backend/tests/test_oracle_adapter.py b/backend/tests/test_oracle_adapter.py new file mode 100644 index 0000000..9c61896 --- /dev/null +++ b/backend/tests/test_oracle_adapter.py @@ -0,0 +1,420 @@ +""" +test_oracle_adapter.py +────────────────────── +Comprehensive test suite for the ORACLE Integration Adapter. + +Test categories (per Issue #7 acceptance criteria) +─────────────────────────────────────────────────── +1. Valid payload normalization +2. Malformed payload rejection +3. Schema compatibility (legacy/future shapes) +4. Evidence mapping regression +5. Deterministic output +6. Adapter isolation (MAIN never sees ORACLE internals) + +Run: + export PYTHONPATH=$PYTHONPATH:$(pwd)/backend + backend/venv/bin/python3 -m pytest backend/tests/test_oracle_adapter.py -v +""" + +from __future__ import annotations + +import copy +import pytest + +# ── Helpers — minimal valid ORACLE payload factory ──────────────────────────── + +def _evidence(value: str, confidence: float = 0.9, evidence: list | None = None) -> dict: + return {"value": value, "confidence": confidence, "evidence": evidence or []} + + +def _viva(topic: str, question_target: str, category: str = "Architecture") -> dict: + return { + "topic": topic, + "question_target": question_target, + "difficulty": "medium", + "importance_score": 0.8, + "depth_score": 7.0, + "focus": f"Explain {question_target}", + "category": category, + "related_node": "api_gateway", + "confidence": 0.85, + "reasoning_summary": "Test target", + "evidence": ["line 42 of routes.py"], + } + + +def _min_valid_payload(**overrides) -> dict: + """Return a minimal valid ORACLE payload dict.""" + base = { + "project_name": _evidence("Test Project"), + "project_type": _evidence("Web Application"), + "backend_framework": _evidence("FastAPI"), + "frontend_framework": _evidence("React"), + "database_used": _evidence("PostgreSQL"), + "authentication_system": _evidence("JWT"), + "architecture_pattern": _evidence("REST API"), + "viva_intelligence_targets": [_viva("Architecture", "REST Constraints")], + "implementation_viva_targets": [], + "failure_paths": [], + "runtime_risks": [], + "inconsistencies": [], + "middleware_chain": [], + "security_flows": [], + "execution_graph": { + "nodes": [{"id": "n1", "label": "Route", "type": "ROUTE", "metadata": {}}], + "edges": [{"source": "n1", "target": "n1", "relationship": "calls", "confidence": 0.9, "evidence": []}], + "middleware": ["cors"], + "db_calls": ["query_user"], + "auth_points": ["verify_token"], + "risk_flags": [], + "failure_paths": [], + }, + "complexity_mismatch": _evidence("No major mismatch detected", 1.0, []), + } + base.update(overrides) + return base + + +# ══════════════════════════════════════════════════════════════════════════════ +# 1. VALID PAYLOAD NORMALIZATION +# ══════════════════════════════════════════════════════════════════════════════ + +class TestValidPayloadNormalization: + + def test_project_identity_fields_mapped(self): + from src.agents.main_agent.integration import OracleAdapter + out = OracleAdapter.from_dict(_min_valid_payload()) + assert out.project_name == "Test Project" + assert out.project_type == "Web Application" + assert out.architecture_pattern == "REST API" + + def test_observable_signals_populated(self): + from src.agents.main_agent.integration import OracleAdapter + out = OracleAdapter.from_dict(_min_valid_payload()) + keys = {s.key for s in out.observable_signals} + assert "backend_framework" in keys + assert "frontend_framework" in keys + + def test_observable_signal_values_correct(self): + from src.agents.main_agent.integration import OracleAdapter + out = OracleAdapter.from_dict(_min_valid_payload()) + be = next(s for s in out.observable_signals if s.key == "backend_framework") + assert be.value == "FastAPI" + assert 0.0 <= be.confidence <= 1.0 + + def test_viva_targets_normalized(self): + from src.agents.main_agent.integration import OracleAdapter + out = OracleAdapter.from_dict(_min_valid_payload()) + assert len(out.viva_targets) == 1 + t = out.viva_targets[0] + assert t.topic == "Architecture" + assert t.question_target == "REST Constraints" + + def test_execution_graph_summary_counts(self): + from src.agents.main_agent.integration import OracleAdapter + out = OracleAdapter.from_dict(_min_valid_payload()) + assert out.execution_node_count == 1 + assert out.execution_edge_count == 1 + assert out.middleware_count == 1 + assert out.auth_point_count == 1 + + def test_no_adapter_warnings_on_valid_payload(self): + from src.agents.main_agent.integration import OracleAdapter + out = OracleAdapter.from_dict(_min_valid_payload()) + assert out.adapter_warnings == [] + + def test_complexity_mismatch_not_detected_for_no_mismatch(self): + from src.agents.main_agent.integration import OracleAdapter + out = OracleAdapter.from_dict(_min_valid_payload()) + assert out.complexity_mismatch_detected is False + + def test_complexity_mismatch_detected_when_present(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + complexity_mismatch=_evidence("High Complexity Claim vs Minimal Implementation", 0.7) + ) + out = OracleAdapter.from_dict(payload) + assert out.complexity_mismatch_detected is True + assert "High Complexity" in out.complexity_mismatch_note + + def test_schema_version_is_set(self): + from src.agents.main_agent.integration import OracleAdapter + from src.agents.main_agent.integration.oracle_schema import ADAPTER_SCHEMA_VERSION + out = OracleAdapter.from_dict(_min_valid_payload()) + assert out.schema_version == ADAPTER_SCHEMA_VERSION + + def test_both_viva_lists_merged_and_deduplicated(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + viva_intelligence_targets=[_viva("Architecture", "Unique Legacy")], + implementation_viva_targets=[_viva("Security", "JWT Lifecycle"), _viva("Architecture", "Unique Legacy")], + ) + out = OracleAdapter.from_dict(payload) + targets_by_qt = [t.question_target for t in out.viva_targets] + # Dedup: "Unique Legacy" should appear once + assert targets_by_qt.count("Unique Legacy") == 1 + assert any(t.question_target == "JWT Lifecycle" for t in out.viva_targets) + + def test_failure_paths_from_evidence_models(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + failure_paths=[{"value": "DB timeout cascade", "confidence": 0.8, "evidence": ["line 99"]}] + ) + out = OracleAdapter.from_dict(payload) + assert len(out.failure_scenarios) == 1 + assert "DB timeout" in out.failure_scenarios[0].description + + def test_runtime_risks_become_failure_scenarios(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + runtime_risks=[{"value": "Memory leak in handler", "severity": "HIGH", "confidence": 0.75, "evidence": []}] + ) + out = OracleAdapter.from_dict(payload) + assert any("Memory leak" in s.description for s in out.failure_scenarios) + + def test_inconsistencies_normalized(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + inconsistencies=[{ + "issue": "Redis mentioned but absent", + "severity": "medium", + "confidence": 0.85, + "evidence": ["doc line 5"], + }] + ) + out = OracleAdapter.from_dict(payload) + assert len(out.inconsistencies) == 1 + assert "Redis" in out.inconsistencies[0].issue + + +# ══════════════════════════════════════════════════════════════════════════════ +# 2. MALFORMED PAYLOAD REJECTION +# ══════════════════════════════════════════════════════════════════════════════ + +class TestMalformedPayloadRejection: + + def test_non_dict_returns_safe_fallback(self): + from src.agents.main_agent.integration import OracleAdapter + for bad in [None, [], "string", 42, object()]: + out = OracleAdapter.from_dict(bad) + assert isinstance(out.adapter_warnings, list) + assert len(out.adapter_warnings) > 0 + + def test_missing_required_key_returns_safe_fallback(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + del payload["backend_framework"] + out = OracleAdapter.from_dict(payload) + assert len(out.adapter_warnings) > 0 + + def test_required_field_not_dict_returns_safe_fallback(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + payload["project_name"] = "raw string not a dict" + out = OracleAdapter.from_dict(payload) + assert len(out.adapter_warnings) > 0 + + def test_evidence_model_missing_value_key_returns_safe_fallback(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + payload["project_name"] = {"confidence": 0.9, "evidence": []} # no 'value' + out = OracleAdapter.from_dict(payload) + assert len(out.adapter_warnings) > 0 + + def test_safe_fallback_has_sensible_defaults(self): + from src.agents.main_agent.integration import OracleAdapter + out = OracleAdapter.from_dict(None) + assert out.project_name == "Unknown" + assert out.viva_targets == [] + assert out.failure_scenarios == [] + assert out.observable_signals == [] + + +# ══════════════════════════════════════════════════════════════════════════════ +# 3. SCHEMA COMPATIBILITY (legacy/future shapes) +# ══════════════════════════════════════════════════════════════════════════════ + +class TestSchemaCompatibility: + + def test_legacy_viva_only_field_accepted(self): + """Payload with only viva_intelligence_targets (no implementation_viva_targets).""" + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + del payload["implementation_viva_targets"] + out = OracleAdapter.from_dict(payload) + assert len(out.viva_targets) == 1 + + def test_project_graph_shimmed_to_execution_graph(self): + """Payload using old project_graph key should still produce graph summary.""" + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + del payload["execution_graph"] + payload["project_graph"] = { + "nodes": [{"id": "n1", "label": "X", "type": "ROUTE", "metadata": {}}], + "edges": [], + } + out = OracleAdapter.from_dict(payload) + assert out.execution_node_count == 1 + + def test_flat_string_failure_paths_coerced(self): + """Old ORACLE builds that emitted failure_paths as List[str].""" + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload(failure_paths=["DB went down", "Auth timed out"]) + out = OracleAdapter.from_dict(payload) + assert len(out.failure_scenarios) == 2 + + def test_missing_evidence_list_in_evidence_model_handled(self): + """EvidenceModel dicts without 'evidence' key.""" + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + payload["backend_framework"] = {"value": "FastAPI", "confidence": 0.95} # no 'evidence' + out = OracleAdapter.from_dict(payload) + be = next((s for s in out.observable_signals if s.key == "backend_framework"), None) + assert be is not None + assert be.value == "FastAPI" + + def test_lowercase_runtime_risk_severity_coerced(self): + """Old payloads using lowercase severity strings.""" + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + runtime_risks=[{"value": "OOM risk", "severity": "critical", "confidence": 0.9, "evidence": []}] + ) + out = OracleAdapter.from_dict(payload) + assert any(s.description == "OOM risk" for s in out.failure_scenarios) + + +# ══════════════════════════════════════════════════════════════════════════════ +# 4. EVIDENCE MAPPING REGRESSION +# ══════════════════════════════════════════════════════════════════════════════ + +class TestEvidenceMapping: + + def test_viva_target_evidence_preserved(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + viva_intelligence_targets=[{ + **_viva("Security", "JWT Lifecycle"), + "evidence": ["jwt middleware detected at line 88", "token expiry = 3600s"], + }], + implementation_viva_targets=[], + ) + out = OracleAdapter.from_dict(payload) + t = next(t for t in out.viva_targets if t.question_target == "JWT Lifecycle") + assert len(t.evidence) == 2 + assert t.evidence[0].text == "jwt middleware detected at line 88" + + def test_observable_signal_evidence_preserved(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + payload["backend_framework"]["evidence"] = ["import fastapi found in main.py:3"] + out = OracleAdapter.from_dict(payload) + be = next(s for s in out.observable_signals if s.key == "backend_framework") + assert be.evidence[0].text == "import fastapi found in main.py:3" + + def test_failure_scenario_evidence_preserved(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + failure_paths=[{"value": "Cache miss storm", "confidence": 0.8, "evidence": ["redis timeout line 42"]}] + ) + out = OracleAdapter.from_dict(payload) + fs = next(s for s in out.failure_scenarios if "Cache miss" in s.description) + assert fs.evidence[0].text == "redis timeout line 42" + + def test_inconsistency_evidence_preserved(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + inconsistencies=[{ + "issue": "Redis missing", + "severity": "medium", + "confidence": 0.9, + "evidence": ["doc mentions Redis", "requirements.txt has no redis"], + }] + ) + out = OracleAdapter.from_dict(payload) + inc = out.inconsistencies[0] + assert len(inc.evidence) == 2 + + +# ══════════════════════════════════════════════════════════════════════════════ +# 5. DETERMINISTIC OUTPUT +# ══════════════════════════════════════════════════════════════════════════════ + +class TestDeterministicOutput: + + def test_same_input_produces_same_output(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + out1 = OracleAdapter.from_dict(copy.deepcopy(payload)) + out2 = OracleAdapter.from_dict(copy.deepcopy(payload)) + assert out1.model_dump() == out2.model_dump() + + def test_adapter_does_not_mutate_input(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload() + original = copy.deepcopy(payload) + OracleAdapter.from_dict(payload) + assert payload == original + + def test_empty_lists_yield_empty_outputs(self): + from src.agents.main_agent.integration import OracleAdapter + payload = _min_valid_payload( + viva_intelligence_targets=[], + implementation_viva_targets=[], + failure_paths=[], + runtime_risks=[], + inconsistencies=[], + ) + out = OracleAdapter.from_dict(payload) + assert out.viva_targets == [] + assert out.failure_scenarios == [] + assert out.inconsistencies == [] + + +# ══════════════════════════════════════════════════════════════════════════════ +# 6. ADAPTER ISOLATION +# ══════════════════════════════════════════════════════════════════════════════ + +class TestAdapterIsolation: + + def test_output_type_is_normalized_not_structured_context(self): + """MAIN must never receive a StructuredContext object.""" + from src.agents.main_agent.integration import OracleAdapter + from src.agents.main_agent.integration.oracle_schema import NormalizedOracleOutput + out = OracleAdapter.from_dict(_min_valid_payload()) + assert isinstance(out, NormalizedOracleOutput) + + def test_output_has_no_oracle_internal_types(self): + """NormalizedOracleOutput fields must not contain ORACLE-internal objects.""" + from src.agents.main_agent.integration import OracleAdapter + from src.models.context import StructuredContext, EvidenceModel, VivaTarget + out = OracleAdapter.from_dict(_min_valid_payload()) + dumped = out.model_dump() + # Recursively check no pydantic ORACLE model sneaked through + def _has_oracle_type(obj): + if isinstance(obj, (StructuredContext, EvidenceModel, VivaTarget)): + return True + if isinstance(obj, dict): + return any(_has_oracle_type(v) for v in obj.values()) + if isinstance(obj, list): + return any(_has_oracle_type(i) for i in obj) + return False + assert not _has_oracle_type(out) + + def test_from_context_accepts_structured_context(self): + """OracleAdapter.from_context must accept a real StructuredContext.""" + from src.agents.main_agent.integration import OracleAdapter, NormalizedOracleOutput + from src.models.context import StructuredContext, EvidenceModel, ExecutionGraph + ctx = StructuredContext( + project_name = EvidenceModel(value="My Project", confidence=0.9, evidence=[]), + project_type = EvidenceModel(value="API", confidence=0.9, evidence=[]), + frontend_framework = EvidenceModel(value="React", confidence=0.85, evidence=[]), + backend_framework = EvidenceModel(value="FastAPI", confidence=0.95, evidence=[]), + database_used = EvidenceModel(value="PostgreSQL", confidence=0.9, evidence=[]), + authentication_system = EvidenceModel(value="JWT", confidence=0.9, evidence=[]), + architecture_pattern = EvidenceModel(value="REST API", confidence=0.9, evidence=[]), + execution_graph = ExecutionGraph(), + ) + out = OracleAdapter.from_context(ctx) + assert isinstance(out, NormalizedOracleOutput) + assert out.project_name == "My Project" From ec829fc735dbff79e9c54d1e820dacd1a7ad9572 Mon Sep 17 00:00:00 2001 From: Aman koli <2025.amana@isu.ac.in> Date: Tue, 19 May 2026 20:30:35 +0530 Subject: [PATCH 2/2] chore: remove node_modules from tracking, fix .gitignore - Added node_modules/ to .gitignore - Removed backend/node_modules from git cache (should never be committed) - Added backend/cloned_repos/ to .gitignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 73d6aa6..45f33f8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,12 @@ venv/ ENV/ .env +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + # IDEs .vscode/ .idea/ @@ -17,5 +23,6 @@ ENV/ # TWELVE Data backend/data/ +backend/cloned_repos/ backend/evaluation/results/ backend/tests/validation_report.html