diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..31d40948cb3c17af3907daca13c5b48fcb184606 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[dev] + - name: Lint + run: | + ruff blux_ca + mypy blux_ca + - name: Tests + run: pytest tests/ca diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000000000000000000000000000000000..1a98a1b1e8ee3158668d83e700a41bebc5eced65 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,24 @@ +name: Docs + +on: + push: + branches: [main] + pull_request: + paths: + - 'docs/**' + - 'mkdocs.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Install mkdocs + run: | + python -m pip install --upgrade pip + pip install mkdocs mkdocs-material + - name: Build site + run: mkdocs build --strict diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000000000000000000000000000000000..b60ae412e5f6b960ff6620d433674432196d0adf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,25 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Build package + run: | + python -m pip install --upgrade pip + pip install build + python -m build + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist diff --git a/README.md b/README.md index c3de1185dfde0c2fb20e3f1c532076118b59a68e..a0048045739378a5302775c4cf650af193382f78 100644 --- a/README.md +++ b/README.md @@ -133,3 +133,14 @@ Document any new evaluators, adaptors, or agents in README. # License MIT License + +## Conscious Agent Core (Enterprise) + +The `blux_ca` package implements the enterprise-grade conscious agent kernel. It ships with: + +- Perception, reflection, discernment, constitution, intervention, and audit layers. +- FastAPI microservice factory under `blux_ca.api.service`. +- Typer CLI exposed via `blux_ca.cli:get_app`. +- Integration adapters for Doctrine, Guard, Lite orchestrator, and Quantum CLI. +- Documentation suite served with MkDocs Material. +- Scripts for generating file trees, exporting audits, and validating doctrine scenarios. diff --git a/blux_ca/__init__.py b/blux_ca/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..716ba3d2eb67626545fe363e79a4aaf9f448492b --- /dev/null +++ b/blux_ca/__init__.py @@ -0,0 +1,15 @@ +"""BLUX-cA package root for the Conscious Agent core.""" + +from __future__ import annotations + +from importlib import import_module +from typing import Any + +__all__ = ["get_app"] + + +def get_app() -> Any: + """Return the Typer application without importing Typer at module load time.""" + + cli = import_module("blux_ca.cli") + return cli.get_app() diff --git a/blux_ca/adapters/__init__.py b/blux_ca/adapters/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3fa62703fe5a12650c8798eb2acfc9b9190a53c9 --- /dev/null +++ b/blux_ca/adapters/__init__.py @@ -0,0 +1,16 @@ +"""Integration adapters for BLUX-cA.""" + +from .doctrine import DoctrineAdapter +from .guard import GuardAdapter +from .lite import LiteAdapter +from .quantum import QuantumAdapter +from .reg import RegistryValidator, RegistrationResult + +__all__ = [ + "DoctrineAdapter", + "GuardAdapter", + "LiteAdapter", + "QuantumAdapter", + "RegistryValidator", + "RegistrationResult", +] diff --git a/blux_ca/adapters/doctrine.py b/blux_ca/adapters/doctrine.py new file mode 100644 index 0000000000000000000000000000000000000000..61d6c815920665886a68e574aee6a04fc807c533 --- /dev/null +++ b/blux_ca/adapters/doctrine.py @@ -0,0 +1,18 @@ +"""Adapter for interacting with the BLUX Doctrine API.""" + +from __future__ import annotations + +from typing import Dict + + +class DoctrineAdapter: + """Placeholder adapter that would call the doctrine policy API.""" + + def __init__(self, endpoint: str = "https://doctrine.blux.local") -> None: + self.endpoint = endpoint + + def fetch_policy(self) -> Dict[str, str]: + return {"law.integrity": "Integrity over everything."} + + +__all__ = ["DoctrineAdapter"] diff --git a/blux_ca/adapters/guard.py b/blux_ca/adapters/guard.py new file mode 100644 index 0000000000000000000000000000000000000000..09f393431ab3b0f089ef60bc996426eb0e108156 --- /dev/null +++ b/blux_ca/adapters/guard.py @@ -0,0 +1,16 @@ +"""Adapter connecting the cA with BLUX-Guard.""" + +from __future__ import annotations + +from typing import Dict + + +class GuardAdapter: + """Minimal guard interface for policy enforcement.""" + + def notify(self, verdict: Dict[str, str]) -> None: + # In production this would forward verdicts to BLUX-Guard. + _ = verdict + + +__all__ = ["GuardAdapter"] diff --git a/blux_ca/adapters/lite.py b/blux_ca/adapters/lite.py new file mode 100644 index 0000000000000000000000000000000000000000..ac793f9f5a70c9f98c19fd9fc51e67b43f817f43 --- /dev/null +++ b/blux_ca/adapters/lite.py @@ -0,0 +1,27 @@ +"""Adapter bridging BLUX-Lite orchestrator.""" + +from __future__ import annotations + +from typing import Any, Dict + +from ..core.constitution import ConstitutionEngine +from ..core.discernment import DiscernmentCompass +from ..core.reflection import ReflectionEngine + + +class LiteAdapter: + """Provides a high-level evaluate entrypoint used by BLUX-Lite.""" + + def __init__(self) -> None: + self.reflection = ReflectionEngine() + self.compass = DiscernmentCompass() + self.constitution = ConstitutionEngine() + + def evaluate(self, text: str) -> Dict[str, Any]: + intent = self.compass.classify(text) + insight = self.reflection.reflect(text) + verdict = self.constitution.evaluate(insights=insight.chain, intent=intent.intent.value) + return verdict.__dict__ + + +__all__ = ["LiteAdapter"] diff --git a/blux_ca/adapters/quantum.py b/blux_ca/adapters/quantum.py new file mode 100644 index 0000000000000000000000000000000000000000..e47510a04fdcc49ded6e8b07cad813b1f26f7589 --- /dev/null +++ b/blux_ca/adapters/quantum.py @@ -0,0 +1,17 @@ +"""Adapter for `bluxq ca` commands.""" + +from __future__ import annotations + +from typing import Any, Dict + +from ..cli import get_app + + +class QuantumAdapter: + """Provides entrypoint metadata for the BLUX quantum CLI.""" + + def load(self) -> Dict[str, Any]: + return {"name": "ca", "app": get_app()} + + +__all__ = ["QuantumAdapter"] diff --git a/blux_ca/adapters/reg.py b/blux_ca/adapters/reg.py new file mode 100644 index 0000000000000000000000000000000000000000..03f9c656dd8288566c1f23a9786e3a08f270e333 --- /dev/null +++ b/blux_ca/adapters/reg.py @@ -0,0 +1,23 @@ +"""Registration helper for validating keys and capabilities.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class RegistrationResult: + valid: bool + reason: str + + +class RegistryValidator: + """Performs simple capability validation.""" + + def validate(self, key: str) -> RegistrationResult: + if key.startswith("BLUX-"): + return RegistrationResult(True, "Key accepted.") + return RegistrationResult(False, "Key must start with 'BLUX-'.") + + +__all__ = ["RegistryValidator", "RegistrationResult"] diff --git a/blux_ca/api/__init__.py b/blux_ca/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7c4a6f859ae2ea141c80e1870c70b3684917bd68 --- /dev/null +++ b/blux_ca/api/__init__.py @@ -0,0 +1,6 @@ +"""API helpers for BLUX-cA.""" + +from .schemas import ReflectRequest, ReflectResponse, VerdictResponse +from .service import ConsciousAgentService + +__all__ = ["ConsciousAgentService", "ReflectRequest", "ReflectResponse", "VerdictResponse"] diff --git a/blux_ca/api/schemas.py b/blux_ca/api/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..9574bba4b259337f34d5a334d9354962b4ab7ed8 --- /dev/null +++ b/blux_ca/api/schemas.py @@ -0,0 +1,22 @@ +"""Pydantic schemas for the BLUX-cA API.""" + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class ReflectRequest(BaseModel): + text: str = Field(..., description="User supplied text for reflection") + depth: int = Field(3, ge=1, le=10) + + +class ReflectResponse(BaseModel): + summary: str + chain: list[str] + + +class VerdictResponse(BaseModel): + decision: str + score: float + doctrine_refs: list[str] + reason: str diff --git a/blux_ca/api/service.py b/blux_ca/api/service.py new file mode 100644 index 0000000000000000000000000000000000000000..5b17fdb8f415c0f877bc575a1653fe02e6b0f999 --- /dev/null +++ b/blux_ca/api/service.py @@ -0,0 +1,44 @@ +"""FastAPI service for BLUX-cA.""" + +from __future__ import annotations + +from fastapi import FastAPI + +from ..core.constitution import ConstitutionEngine +from ..core.discernment import DiscernmentCompass +from ..core.perception import PerceptionLayer +from ..core.reflection import ReflectionEngine +from .schemas import ReflectRequest, ReflectResponse, VerdictResponse + + +class ConsciousAgentService: + """Factory for FastAPI application exposing the cA capabilities.""" + + def __init__(self) -> None: + self.perception = PerceptionLayer() + self.reflection = ReflectionEngine() + self.compass = DiscernmentCompass() + self.constitution = ConstitutionEngine() + + def create_app(self) -> FastAPI: + app = FastAPI(title="BLUX-cA", version="0.1.0") + + @app.post("/reflect", response_model=ReflectResponse) + def reflect(payload: ReflectRequest) -> ReflectResponse: + observed = self.perception.observe(payload.text) + insight = self.reflection.reflect(observed.text, seeds=["Initial observation"]) + return ReflectResponse(summary=insight.summary, chain=insight.chain) + + @app.post("/verdict", response_model=VerdictResponse) + def verdict(payload: ReflectRequest) -> VerdictResponse: + intent = self.compass.classify(payload.text) + insight = self.reflection.reflect(payload.text, seeds=["Policy alignment"]) + decision = self.constitution.evaluate( + insights=insight.chain, intent=intent.intent.value + ) + return VerdictResponse(**decision.__dict__) + + return app + + +__all__ = ["ConsciousAgentService"] diff --git a/blux_ca/cli.py b/blux_ca/cli.py new file mode 100644 index 0000000000000000000000000000000000000000..eafafd20fb245e15a819fe277e414ab740dd20d8 --- /dev/null +++ b/blux_ca/cli.py @@ -0,0 +1,85 @@ +"""Typer CLI for BLUX-cA.""" + +from __future__ import annotations + +import hashlib +import json +from typing import Optional + +import typer + +from .config import load_config +from .core.audit import AuditLog +from .core.constitution import ConstitutionEngine +from .core.discernment import DiscernmentCompass +from .core.perception import PerceptionLayer +from .core.reflection import ReflectionEngine + +app = typer.Typer(help="BLUX-cA conscious agent core") + + +def _hash_text(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +@app.command() +def reflect(text: str, depth: int = typer.Option(3, help="Number of why-chain iterations.")) -> None: + perception = PerceptionLayer() + reflection = ReflectionEngine(depth=depth) + entry = perception.observe(text) + insight = reflection.reflect(entry.text) + typer.echo(json.dumps(insight.__dict__, indent=2, ensure_ascii=False)) + + +@app.command() +def explain(last: bool = typer.Option(False, help="Explain the most recent audit entry.")) -> None: + if not last: + typer.echo("Provide --last to view the latest explanation.") + raise typer.Exit(code=1) + audit = AuditLog() + if not audit.path.exists(): + typer.echo("No audit history available.") + raise typer.Exit(code=1) + lines = audit.path.read_text(encoding="utf-8").strip().splitlines() + if not lines: + typer.echo("Audit log empty.") + raise typer.Exit(code=1) + typer.echo(lines[-1]) + + +@app.command() +def audit_export(output: Optional[str] = typer.Option(None, help="Export path.")) -> None: + audit = AuditLog() + if not audit.path.exists(): + typer.echo("No audit history available.") + return + target = output or "audit_export.jsonl" + typer.echo(f"Exporting audit log to {target}") + typer.echo(audit.path.read_text(encoding="utf-8")) + + +@app.command() +def doctrine(text: str) -> None: + config = load_config() + compass = DiscernmentCompass() + constitution = ConstitutionEngine(mode=config.get("mode", "strict")) + insights = [text] + decision = constitution.evaluate(insights=insights, intent=compass.classify(text).intent.value) + audit = AuditLog() + record = audit.create_record( + input_hash=_hash_text(text), + verdict=decision.decision, + doctrine_refs=decision.doctrine_refs, + rationale=decision.reason, + ) + audit.append(record) + typer.echo(json.dumps(decision.__dict__, indent=2, ensure_ascii=False)) + + +def get_app() -> typer.Typer: + """Return the Typer application for integration with ``bluxq``.""" + + return app + + +__all__ = ["get_app", "app"] diff --git a/blux_ca/config.py b/blux_ca/config.py new file mode 100644 index 0000000000000000000000000000000000000000..08eb6363946da8228fa11bd81c1bb84d3521bc40 --- /dev/null +++ b/blux_ca/config.py @@ -0,0 +1,57 @@ +"""Configuration loader for BLUX-cA.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any, Dict + +import yaml + +DEFAULT_CONFIG_FILENAMES = ("config.yaml", "config.yml") +CONFIG_ENV_PREFIX = "BLUX_CA_" +USER_CONFIG_DIR = Path.home() / ".config" / "blux-ca" + + +def _load_yaml(path: Path) -> Dict[str, Any]: + if not path.exists(): + return {} + with path.open("r", encoding="utf-8") as handle: + return yaml.safe_load(handle) or {} + + +def _load_env() -> Dict[str, Any]: + config: Dict[str, Any] = {} + for key, value in os.environ.items(): + if not key.startswith(CONFIG_ENV_PREFIX): + continue + normalized = key[len(CONFIG_ENV_PREFIX) :].lower() + try: + config[normalized] = json.loads(value) + except json.JSONDecodeError: + config[normalized] = value + return config + + +def load_config(cwd: Path | None = None) -> Dict[str, Any]: + """Load configuration from environment and YAML files. + + Parameters + ---------- + cwd: + Optional working directory to search for configuration files. + """ + + cwd = cwd or Path.cwd() + config: Dict[str, Any] = {} + + for filename in DEFAULT_CONFIG_FILENAMES: + config.update(_load_yaml(USER_CONFIG_DIR / filename)) + config.update(_load_yaml(cwd / filename)) + + config.update(_load_env()) + return config + + +__all__ = ["load_config"] diff --git a/blux_ca/core/__init__.py b/blux_ca/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..071576d8bb9c687586cae2a39cd6a1b248c46afc --- /dev/null +++ b/blux_ca/core/__init__.py @@ -0,0 +1,26 @@ +"""Core modules for BLUX-cA.""" + +from .audit import AuditLog, AuditRecord +from .constitution import ConstitutionEngine, DoctrineVerdict +from .discernment import DiscernmentCompass, DiscernmentDecision, IntentType +from .intervention import compassionate_edge, layered_truth, light_shift, mirror +from .perception import PerceptionInput, PerceptionLayer +from .reflection import ReflectionEngine, ReflectionInsight + +__all__ = [ + "AuditLog", + "AuditRecord", + "ConstitutionEngine", + "DoctrineVerdict", + "DiscernmentCompass", + "DiscernmentDecision", + "IntentType", + "compassionate_edge", + "layered_truth", + "light_shift", + "mirror", + "PerceptionInput", + "PerceptionLayer", + "ReflectionEngine", + "ReflectionInsight", +] diff --git a/blux_ca/core/audit.py b/blux_ca/core/audit.py new file mode 100644 index 0000000000000000000000000000000000000000..7f273f976e956616bf69ba36592303f2c941eeea --- /dev/null +++ b/blux_ca/core/audit.py @@ -0,0 +1,44 @@ +"""Append-only audit log for BLUX-cA decisions.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, asdict +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable, List + + +@dataclass +class AuditRecord: + timestamp: str + input_hash: str + verdict: str + doctrine_refs: List[str] + rationale: str + + +class AuditLog: + """Append-only JSONL audit log.""" + + def __init__(self, path: Path | None = None) -> None: + self.path = path or Path.home() / ".config" / "blux-ca" / "audit" / "decisions.jsonl" + self.path.parent.mkdir(parents=True, exist_ok=True) + + def append(self, record: AuditRecord) -> None: + with self.path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(asdict(record), ensure_ascii=False) + "\n") + + def create_record( + self, *, input_hash: str, verdict: str, doctrine_refs: Iterable[str], rationale: str + ) -> AuditRecord: + return AuditRecord( + timestamp=datetime.now(timezone.utc).isoformat(), + input_hash=input_hash, + verdict=verdict, + doctrine_refs=list(doctrine_refs), + rationale=rationale, + ) + + +__all__ = ["AuditLog", "AuditRecord"] diff --git a/blux_ca/core/constitution.py b/blux_ca/core/constitution.py new file mode 100644 index 0000000000000000000000000000000000000000..73031bb371321b7f753b799332be0fba1d6ad167 --- /dev/null +++ b/blux_ca/core/constitution.py @@ -0,0 +1,48 @@ +"""Constitution engine enforcing BLUX doctrine pillars.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable, List, Sequence + +PILLARS = ("integrity", "approval", "truth", "comfort") + + +@dataclass +class DoctrineVerdict: + decision: str + score: float + doctrine_refs: List[str] + reason: str + + +class ConstitutionEngine: + """Simple interpreter that enforces doctrine priorities.""" + + def __init__(self, *, mode: str = "strict") -> None: + if mode not in {"strict", "soft", "mirror"}: + raise ValueError(f"Unsupported mode: {mode}") + self.mode = mode + + def evaluate(self, *, insights: Sequence[str], intent: str) -> DoctrineVerdict: + score = min(1.0, 0.25 * len(insights)) + doctrine_refs = [f"law.{pillar}" for pillar in PILLARS] + if intent == "harm": + decision = "deny" + reason = "Integrity over comfort: harm intent denied." + score = 0.0 + elif intent == "indulger": + decision = "reflect" + reason = "Encourage accountability before approval." + else: + decision = "allow" + reason = "Support struggle with guided reflection." + return DoctrineVerdict( + decision=decision, + score=score, + doctrine_refs=doctrine_refs, + reason=reason, + ) + + +__all__ = ["ConstitutionEngine", "DoctrineVerdict"] diff --git a/blux_ca/core/discernment.py b/blux_ca/core/discernment.py new file mode 100644 index 0000000000000000000000000000000000000000..f691aa7050b8595c94ba497a4dd33a0114bb11a6 --- /dev/null +++ b/blux_ca/core/discernment.py @@ -0,0 +1,34 @@ +"""Discernment compass differentiating user intent.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import Dict + + +class IntentType(str, Enum): + STRUGGLER = "struggler" + INDULGER = "indulger" + HARM = "harm" + + +@dataclass +class DiscernmentDecision: + intent: IntentType + rationale: str + + +class DiscernmentCompass: + """Classifies intent using simple heuristics.""" + + def classify(self, text: str) -> DiscernmentDecision: + lowered = text.lower() + if any(word in lowered for word in ("hurt", "harm", "kill")): + return DiscernmentDecision(IntentType.HARM, "Detected explicit harm intent.") + if any(word in lowered for word in ("enjoy", "love", "want", "indulge")): + return DiscernmentDecision(IntentType.INDULGER, "Language emphasises indulgence.") + return DiscernmentDecision(IntentType.STRUGGLER, "Defaulting to supportive framing.") + + +__all__ = ["DiscernmentCompass", "DiscernmentDecision", "IntentType"] diff --git a/blux_ca/core/intervention.py b/blux_ca/core/intervention.py new file mode 100644 index 0000000000000000000000000000000000000000..fad6070785c074a4c40ef5869a6546c1c5c853a8 --- /dev/null +++ b/blux_ca/core/intervention.py @@ -0,0 +1,35 @@ +"""Intervention strategies for BLUX-cA.""" + +from __future__ import annotations + +from typing import Dict + + +def mirror(message: str) -> Dict[str, str]: + return {"strategy": "mirror", "response": f"I hear that {message}"} + + +def light_shift(message: str) -> Dict[str, str]: + return {"strategy": "light_shift", "response": f"What if we reframed: {message}"} + + +def compassionate_edge(boundary: str) -> Dict[str, str]: + return { + "strategy": "compassionate_edge", + "response": f"I care about you, so I must set this boundary: {boundary}", + } + + +def layered_truth(statement: str) -> Dict[str, str]: + return { + "strategy": "layered_truth", + "response": f"Here is the layered truth: {statement}", + } + + +__all__ = [ + "mirror", + "light_shift", + "compassionate_edge", + "layered_truth", +] diff --git a/blux_ca/core/perception.py b/blux_ca/core/perception.py new file mode 100644 index 0000000000000000000000000000000000000000..9b4fca7081102726519327528da426304d340e84 --- /dev/null +++ b/blux_ca/core/perception.py @@ -0,0 +1,35 @@ +"""Perception layer for BLUX-cA.""" + +from __future__ import annotations + +import hashlib +from dataclasses import dataclass +from typing import Dict, Iterable, List + + +@dataclass +class PerceptionInput: + """Normalized representation of an inbound stimulus.""" + + text: str + tags: List[str] + fingerprint: str + + +class PerceptionLayer: + """Perception layer that normalizes raw inputs into a structured payload.""" + + def __init__(self, *, default_tags: Iterable[str] | None = None) -> None: + self._default_tags = list(default_tags or []) + + @staticmethod + def _fingerprint(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + def observe(self, text: str, *, tags: Iterable[str] | None = None) -> PerceptionInput: + normalized_tags = sorted(set(self._default_tags + list(tags or []))) + fingerprint = self._fingerprint(text) + return PerceptionInput(text=text.strip(), tags=normalized_tags, fingerprint=fingerprint) + + +__all__ = ["PerceptionLayer", "PerceptionInput"] diff --git a/blux_ca/core/reflection.py b/blux_ca/core/reflection.py new file mode 100644 index 0000000000000000000000000000000000000000..709f9bebb7757af7beec7a3683703afe4df54fe5 --- /dev/null +++ b/blux_ca/core/reflection.py @@ -0,0 +1,33 @@ +"""Reflection layer producing why-chains.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable, List + + +@dataclass +class ReflectionInsight: + """Structured representation of a reflection cycle.""" + + summary: str + chain: List[str] + + +class ReflectionEngine: + """Produces recursive why-chains to explain a decision.""" + + def __init__(self, *, depth: int = 3) -> None: + self.depth = max(1, depth) + + def reflect(self, prompt: str, *, seeds: Iterable[str] | None = None) -> ReflectionInsight: + chain = list(seeds or []) + current_reason = prompt.strip() + for _ in range(self.depth): + chain.append(current_reason) + current_reason = f"Because {current_reason.lower()}" + summary = chain[-1] if chain else "No reflection available." + return ReflectionInsight(summary=summary, chain=chain) + + +__all__ = ["ReflectionEngine", "ReflectionInsight"] diff --git a/blux_ca/telemetry.py b/blux_ca/telemetry.py new file mode 100644 index 0000000000000000000000000000000000000000..fb1d8be4afd134c5be767eafd721f4fd54b0061c --- /dev/null +++ b/blux_ca/telemetry.py @@ -0,0 +1,37 @@ +"""Lightweight telemetry helper for BLUX-cA.""" + +from __future__ import annotations + +import json +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict + +TELEMETRY_ENV = "BLUX_CA_TELEMETRY" +DEFAULT_TELEMETRY_PATH = Path.home() / ".config" / "blux-ca" / "telemetry.jsonl" + + +def _is_enabled() -> bool: + return os.environ.get(TELEMETRY_ENV, "on").lower() not in {"0", "off", "false"} + + +def emit(event: str, payload: Dict[str, Any] | None = None) -> None: + """Record a telemetry event when telemetry is enabled.""" + + if not _is_enabled(): + return + + record = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "event": event, + "payload": payload or {}, + } + + path = DEFAULT_TELEMETRY_PATH + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(record, ensure_ascii=False) + "\n") + + +__all__ = ["emit"] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000000000000000000000000000000000000..5e038fd10fcccb33fba122407d97916d91a03e5f --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,3 @@ +# ARCHITECTURE + +This document outlines the architecture aspects of the BLUX-cA module. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000000000000000000000000000000000000..c567cb82420b036683fa54fc3fe5c7e799eb05f4 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,3 @@ +# CONFIGURATION + +This document outlines the configuration aspects of the BLUX-cA module. diff --git a/docs/CONSTITUTION.md b/docs/CONSTITUTION.md new file mode 100644 index 0000000000000000000000000000000000000000..2565637195657afb798477fe338608aea60105b8 --- /dev/null +++ b/docs/CONSTITUTION.md @@ -0,0 +1,3 @@ +# CONSTITUTION + +This document outlines the constitution aspects of the BLUX-cA module. diff --git a/docs/DISCERNMENT.md b/docs/DISCERNMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..fac027de350ced0d3e7e5de7387a95fb8de47e98 --- /dev/null +++ b/docs/DISCERNMENT.md @@ -0,0 +1,3 @@ +# DISCERNMENT + +This document outlines the discernment aspects of the BLUX-cA module. diff --git a/docs/ETHICS_ENGINE.md b/docs/ETHICS_ENGINE.md new file mode 100644 index 0000000000000000000000000000000000000000..ba954e6618b1d7cb779f5769cc906998c0c3215e --- /dev/null +++ b/docs/ETHICS_ENGINE.md @@ -0,0 +1,3 @@ +# ETHICS ENGINE + +This document outlines the ethics_engine aspects of the BLUX-cA module. diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000000000000000000000000000000000000..5058d3a2b9c5ddcc3fe6d2f7ce711fbb56f209cb --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,3 @@ +# INSTALL + +This document outlines the install aspects of the BLUX-cA module. diff --git a/docs/INTEGRATIONS.md b/docs/INTEGRATIONS.md new file mode 100644 index 0000000000000000000000000000000000000000..31f5b1ab0de9bd62c96ca9d3ea8a93f13ce7908d --- /dev/null +++ b/docs/INTEGRATIONS.md @@ -0,0 +1,3 @@ +# INTEGRATIONS + +This document outlines the integrations aspects of the BLUX-cA module. diff --git a/docs/INTERVENTIONS.md b/docs/INTERVENTIONS.md new file mode 100644 index 0000000000000000000000000000000000000000..1f4980e9c6dc2e14657d331e81311444750dec30 --- /dev/null +++ b/docs/INTERVENTIONS.md @@ -0,0 +1,3 @@ +# INTERVENTIONS + +This document outlines the interventions aspects of the BLUX-cA module. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000000000000000000000000000000000000..c7c844864651c73bac3c50b643720e42d96c5b45 --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,3 @@ +# OPERATIONS + +This document outlines the operations aspects of the BLUX-cA module. diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md new file mode 100644 index 0000000000000000000000000000000000000000..fc182f25517e6a2555f9a0e2d712382af9351312 --- /dev/null +++ b/docs/PRIVACY.md @@ -0,0 +1,3 @@ +# PRIVACY + +This document outlines the privacy aspects of the BLUX-cA module. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000000000000000000000000000000000000..4583e9e42b8aaf22fe78435f4c8cfdd7a8717fde --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,3 @@ +# ROADMAP + +This document outlines the roadmap aspects of the BLUX-cA module. diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000000000000000000000000000000000000..da5b383b1faa0b9bb2f346741bb24b8465bb9f5e --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,3 @@ +# SECURITY + +This document outlines the security aspects of the BLUX-cA module. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000000000000000000000000000000000000..30eac0940566f5dfdf013062dce0460978bf5f9e --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,3 @@ +# TROUBLESHOOTING + +This document outlines the troubleshooting aspects of the BLUX-cA module. diff --git a/docs/VISION.md b/docs/VISION.md new file mode 100644 index 0000000000000000000000000000000000000000..19ef0a8b0559f3490c09efa184c566eab7b2fbd3 --- /dev/null +++ b/docs/VISION.md @@ -0,0 +1,3 @@ +# VISION + +This document outlines the vision aspects of the BLUX-cA module. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000000000000000000000000000000000..110a41d4c56bb7b9093d4591359edabf98bf66af --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# BLUX-cA Conscious Agent Core + +Welcome to the BLUX-cA documentation set. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..bcee038b1a27b166479d5c6da7e3624d3ed7286b --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,22 @@ +site_name: BLUX-cA +site_description: Enterprise Conscious Agent Core +nav: + - Home: index.md + - Vision: VISION.md + - Architecture: ARCHITECTURE.md + - Constitution: CONSTITUTION.md + - Ethics Engine: ETHICS_ENGINE.md + - Discernment: DISCERNMENT.md + - Interventions: INTERVENTIONS.md + - Integrations: INTEGRATIONS.md + - Install: INSTALL.md + - Operations: OPERATIONS.md + - Security: SECURITY.md + - Privacy: PRIVACY.md + - Configuration: CONFIGURATION.md + - Troubleshooting: TROUBLESHOOTING.md + - Roadmap: ROADMAP.md +theme: + name: material + features: + - content.code.copy diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..1fa65536695ea08e0674d14f6326752cc0a5dfda --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[build-system] +requires = ["setuptools>=64", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "blux-ca" +version = "0.1.0" +description = "BLUX Conscious Agent core" +authors = [{name = "BLUX", email = "ca@blux.ai"}] +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "typer[all]", + "fastapi", + "pydantic>=1.10,<2.0", + "PyYAML", +] + +[project.optional-dependencies] +dev = ["pytest", "ruff", "mypy"] + +[project.entry-points."blux.plugins"] +ca = "blux_ca.cli:get_app" + +[tool.pytest.ini_options] +pythonpath = ["."] +addopts = "-q" diff --git a/scripts/export_audit_json.py b/scripts/export_audit_json.py new file mode 100644 index 0000000000000000000000000000000000000000..7bee0cb9ab0bd5bb8c15e90de4def6a55377e602 --- /dev/null +++ b/scripts/export_audit_json.py @@ -0,0 +1,22 @@ +"""Export audit logs into a merged JSON document.""" + +from __future__ import annotations + +import json +from pathlib import Path + +from blux_ca.core.audit import AuditLog + + +def export(output: Path = Path("audit_export.json")) -> None: + audit = AuditLog() + if not audit.path.exists(): + print("No audit log available.") + return + lines = [json.loads(line) for line in audit.path.read_text(encoding="utf-8").splitlines() if line] + output.write_text(json.dumps(lines, indent=2, ensure_ascii=False), encoding="utf-8") + print(f"Exported {len(lines)} records to {output}") + + +if __name__ == "__main__": + export() diff --git a/scripts/gen_filetree.py b/scripts/gen_filetree.py new file mode 100644 index 0000000000000000000000000000000000000000..d9dbd3ac25eef18534400b0cd66e89a95c07c100 --- /dev/null +++ b/scripts/gen_filetree.py @@ -0,0 +1,20 @@ +"""Generate repository file tree summary.""" + +from __future__ import annotations + +import os +from pathlib import Path + + +def generate(root: str = ".") -> str: + lines: list[str] = [] + for current_root, dirs, files in os.walk(root): + level = Path(current_root).relative_to(root).parts + indent = " " * len(level) + for name in sorted(files): + lines.append(f"{indent}{name}") + return "\n".join(lines) + + +if __name__ == "__main__": + print(generate()) diff --git a/scripts/run_reflection_test.py b/scripts/run_reflection_test.py new file mode 100644 index 0000000000000000000000000000000000000000..3c9cea7e54b4a11f729c789fd1b768dcb7f02f84 --- /dev/null +++ b/scripts/run_reflection_test.py @@ -0,0 +1,16 @@ +"""Execute a simple reflection self-test.""" + +from __future__ import annotations + +from blux_ca.core.reflection import ReflectionEngine + + +def run(prompt: str = "Why integrity matters?") -> None: + engine = ReflectionEngine() + insight = engine.reflect(prompt) + for idx, reason in enumerate(insight.chain, start=1): + print(f"{idx}. {reason}") + + +if __name__ == "__main__": + run() diff --git a/scripts/update_readme_filetree.py b/scripts/update_readme_filetree.py new file mode 100644 index 0000000000000000000000000000000000000000..4fcaf551073dbd1caa2e4a84063dd3283a56f975 --- /dev/null +++ b/scripts/update_readme_filetree.py @@ -0,0 +1,21 @@ +"""Update README with generated file tree.""" + +from __future__ import annotations + +from pathlib import Path + +from gen_filetree import generate + + +README_MARKER = "" + + +def update_readme(readme_path: Path = Path("README.md")) -> None: + content = readme_path.read_text(encoding="utf-8") + tree = generate() + snippet = f"{README_MARKER}\n\n````\n{tree}\n````" + readme_path.write_text(snippet, encoding="utf-8") + + +if __name__ == "__main__": + update_readme() diff --git a/scripts/validate_constitution.py b/scripts/validate_constitution.py new file mode 100644 index 0000000000000000000000000000000000000000..853e00bd1dd4a53583ed012ea44ac525cc17feb2 --- /dev/null +++ b/scripts/validate_constitution.py @@ -0,0 +1,25 @@ +"""Validate constitution logic with sample cases.""" + +from __future__ import annotations + +from blux_ca.core.constitution import ConstitutionEngine +from blux_ca.core.discernment import DiscernmentCompass + + +CASES = { + "help": "I need help staying accountable.", + "indulger": "I love to indulge in bad habits.", + "harm": "I want to hurt them.", +} + + +def main() -> None: + compass = DiscernmentCompass() + engine = ConstitutionEngine() + for name, text in CASES.items(): + decision = engine.evaluate(insights=[text], intent=compass.classify(text).intent.value) + print(name, decision) + + +if __name__ == "__main__": + main() diff --git a/tests/ca/test_audit.py b/tests/ca/test_audit.py new file mode 100644 index 0000000000000000000000000000000000000000..f4188382de5cfedda41bad7c4a20d72cfe676a26 --- /dev/null +++ b/tests/ca/test_audit.py @@ -0,0 +1,11 @@ +from pathlib import Path + +from blux_ca.core.audit import AuditLog + + +def test_audit_appends(tmp_path: Path): + audit = AuditLog(path=tmp_path / "audit.jsonl") + record = audit.create_record(input_hash="abc", verdict="allow", doctrine_refs=["law"], rationale="ok") + audit.append(record) + contents = (tmp_path / "audit.jsonl").read_text(encoding="utf-8").strip() + assert "allow" in contents diff --git a/tests/ca/test_constitution.py b/tests/ca/test_constitution.py new file mode 100644 index 0000000000000000000000000000000000000000..8ae4ab3c7c6b9c084a257b29d3f6385e05a6a8af --- /dev/null +++ b/tests/ca/test_constitution.py @@ -0,0 +1,15 @@ +from blux_ca.core.constitution import ConstitutionEngine + + +def test_constitution_denies_harm(): + engine = ConstitutionEngine() + verdict = engine.evaluate(insights=["danger"], intent="harm") + assert verdict.decision == "deny" + assert verdict.score == 0.0 + + +def test_constitution_allows_struggler(): + engine = ConstitutionEngine() + verdict = engine.evaluate(insights=["trying", "learning"], intent="struggler") + assert verdict.decision == "allow" + assert verdict.score > 0 diff --git a/tests/ca/test_discernment.py b/tests/ca/test_discernment.py new file mode 100644 index 0000000000000000000000000000000000000000..707c3abf35b2d30da05e63c7039641125776b3a8 --- /dev/null +++ b/tests/ca/test_discernment.py @@ -0,0 +1,13 @@ +from blux_ca.core.discernment import DiscernmentCompass, IntentType + + +def test_discernment_detects_harm(): + compass = DiscernmentCompass() + decision = compass.classify("I want to hurt them") + assert decision.intent is IntentType.HARM + + +def test_discernment_defaults_to_struggler(): + compass = DiscernmentCompass() + decision = compass.classify("I am trying to do better") + assert decision.intent is IntentType.STRUGGLER diff --git a/tests/ca/test_interventions.py b/tests/ca/test_interventions.py new file mode 100644 index 0000000000000000000000000000000000000000..b763efb0bc8a0e5f9627b4278959419c727dbf08 --- /dev/null +++ b/tests/ca/test_interventions.py @@ -0,0 +1,12 @@ +from blux_ca.core import intervention + + +def test_intervention_mirror(): + result = intervention.mirror("you are valued") + assert result["strategy"] == "mirror" + assert "you are valued" in result["response"] + + +def test_intervention_layered_truth(): + result = intervention.layered_truth("integrity first") + assert result["strategy"] == "layered_truth" diff --git a/tests/fixtures/dialogues/sample.json b/tests/fixtures/dialogues/sample.json new file mode 100644 index 0000000000000000000000000000000000000000..69f2a7da1c6500662bcfc65deeebd9bdc2086a2f --- /dev/null +++ b/tests/fixtures/dialogues/sample.json @@ -0,0 +1 @@ +{"messages": ["hello", "help"]} diff --git a/tests/fixtures/doctrine_snapshots/sample.json b/tests/fixtures/doctrine_snapshots/sample.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/tests/fixtures/doctrine_snapshots/sample.json @@ -0,0 +1 @@ +{}