diff --git a/.gitignore b/.gitignore index 539aa6c..d598e4a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ CMakeLists_modified.txt build/ *.egg-info +.venv/ lib/ bin/ @@ -32,3 +33,6 @@ docs/superpowers/ .coverage htmlcov/ coverage.xml + +# Tool/hook caches that get regenerated locally and should not be tracked. +.impeccable/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5eb4d7..2ad7211 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,9 @@ setup and the contribution process. The canonical development workflow live in `AGENTS.md` / `CLAUDE.md`. If you develop with a coding agent, it will pick these up automatically. -## 🚀 Quick Setup +QuantMind is now a **domain library on top of the OpenAI Agents SDK**, not a +self-contained agent framework. The first production flow is finance-first, but +the architecture is intentionally useful for broader agentic knowledge work. 1. **Fork and clone** the repository 2. **Set up environment**: @@ -68,7 +70,7 @@ pytest tests// - **Examples**: one focused example under `examples//` for each new feature. -## 🔄 Pull Request Process +### New domain extension 1. **Create a feature branch** from `master`. 2. **Follow Conventional Commits**: `type(scope): description`, in English. @@ -82,10 +84,10 @@ For significant changes (new modules, new dependencies, API redesigns), open an [issue](https://github.com/LLMQuant/quant-mind/issues) to discuss first. -## ❓ Questions? +## ❓Questions? - Check existing [issues](https://github.com/LLMQuant/quant-mind/issues) - Review architecture patterns in existing code - See `AGENTS.md` / `CLAUDE.md` for repository-wide rules -Thank you for contributing! 🚀 +Thank you for contributing. diff --git a/README.md b/README.md index 25e3e51..98f5473 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

- Transform Financial Knowledge into Actionable Intelligence + Turn Unstructured Documents into Durable, Agent-Ready Knowledge

@@ -30,7 +30,15 @@ --- -**QuantMind** is an intelligent knowledge extraction and retrieval framework for quantitative finance. It transforms unstructured financial content—papers, news, blogs, reports—into a queryable knowledge base, enabling AI-powered research at scale. +**QuantMind** is an agent-centric knowledge extraction library +built on top of the OpenAI Agents SDK. It gives AI agents better eyes over +PDF/HTML/text inputs, typed knowledge instead of brittle strings, and a clean +path toward durable memory and loop-based workflows. + +Today, the first production flow is focused on research-paper extraction. +But the architecture is intentionally broader: the same preprocess → typed +config → typed knowledge → agentic workflow stack can be reused for any +document-heavy domain. ### 📰 News | 🗞️ News | 📝 Description | @@ -40,26 +48,40 @@ ### 🧐 Overview -QuantMind is a next-generation AI platform that ingests, processes, and structures **every** new piece of quantitative-finance research, including papers, news, blogs, and SEC filings into a **semantic knowledge graph**. Institutional investors, hedge funds, and research teams can now explore the frontier of factor strategies, risk models, and market insights in **seconds**, unlocking alpha that would otherwise remain buried. +QuantMind is designed for teams building **serious AI-agent workflows** around +documents, research, and structured memory. -### ✨ Why QuantMind? +Its current strength is typed research extraction, but its core value is +domain-agnostic: -The financial research landscape is overwhelming. Every day, hundreds of papers, articles, and reports are published. +- fetch or accept raw source material from the web, local files, or inline text +- normalize it into markdown that an agent can reliably read +- force output into strict Pydantic knowledge objects +- preserve provenance so downstream agents can review, cite, and reuse results +- support repeatable loops instead of one-off prompt chains -#### 🌐 The Opportunity +### ✨ Why QuantMind? -- **Information Overload**: 500 new research papers & reports published daily. Manual review takes weeks—costly, error-prone, and non-scalable -- **Massive Market**: Financial data & analytics market ≫ expected to grow to US$961.89 billion by 2032, with a compound annual growth rate of 13.5%. Tens of thousands of quant teams & asset managers hungry for speed -- **High ROI**: 1% improvement in research efficiency can translate to millions saved or earned in trading performance +#### For AI agents ---- +- **Better eyes**: `preprocess/` turns PDFs, HTML, and raw text into stable + markdown before the model sees them. +- **Better shared language**: `configs/` and `knowledge/` replace ad-hoc JSON + with typed inputs, typed outputs, and explicit provenance. +- **Better loops**: `magic.py`, `flows/`, and `batch_run()` help agents resolve + intent, execute work, and repeat tasks predictably. +- **Better long-term direction**: the `mind/` roadmap is explicitly about + durable memory and agent-friendly retrieval, not throwaway prompting. -#### 💡 **QuantMind** solves this by +#### For engineering teams -- 🔍 **Extracting** structured knowledge from any source (PDFs, web pages, APIs) -- 🧠 **Understanding** content with domain-specific LLMs fine-tuned for finance -- 💾 **Storing** information in a semantic knowledge graph -- 🚀 **Retrieving** insights through natural language queries +- **Domain-ready by design**: the first production flow is paper-oriented, but + the layering is reusable for any domain that needs document ingestion, typed + extraction, and agentic reuse. +- **Strict architecture**: dependency boundaries are enforced with + `import-linter`, and the repo ships with a single canonical verification loop. +- **Composable by design**: customization happens at three levels—config, + flow kwargs, or forking a flow file. --- @@ -67,37 +89,50 @@ The financial research landscape is overwhelming. Every day, hundreds of papers, ![quantmind-outline](assets/quantmind-stage-outline.png) -QuantMind is built on a decoupled, two-stage architecture. This design separates the concerns of data ingestion from intelligent retrieval, ensuring both robustness and flexibility. +QuantMind is built on a decoupled architecture that separates source handling, +typed extraction, and future memory/store layers. -#### **Stage 1: Knowledge Extraction** - -This layer is responsible for collecting, parsing, and structuring raw information into standardized knowledge units. +#### **Current production path** ```text -Source APIs (arXiv, News, Blogs) → Intelligent Parser → Workflow/Agent → Structured Knowledge Base +Natural-language intent + ↓ +magic.resolve_magic_input(...) + ↓ +typed input + typed cfg + ↓ +preprocess.fetch + preprocess.format + ↓ +flow(agent=OpenAI Agents SDK) + ↓ +typed knowledge object with provenance ``` -- **Source**: Connects to various sources (academic APIs, news feeds, financial blogs, perplexity search source) to pull content -- **Parser**: Extracts text, tables, and figures from PDFs, HTML, and other formats -- **Tagger**: Automatically categorizes content into research areas and topics -- **Workflow/Agent**: Orchestrates the extraction pipeline with quality control and deduplication - -#### **Stage 2: Intelligent Retrieval** +#### **Permanent modules** -This layer transforms structured knowledge into actionable insights through various retrieval mechanisms. - -``` -Knowledge Base → Embeddings → Solution Scenarios (DeepResearch, RAG, Data MCP, ...) -``` +- `quantmind/flows/` — apex layer (`paper_flow`, `batch_run`, observability) +- `quantmind/configs/` — typed inputs and flow configuration +- `quantmind/knowledge/` — typed knowledge shapes and provenance contracts +- `quantmind/preprocess/` — fetch + format + cleaning helpers +- `quantmind/magic.py` — natural language to typed `(input, cfg)` resolution +- `quantmind/mind/` — hybrid memory primitives (L1/L2/L3) +- `quantmind/flows/governance.*` — policy loader + runtime gates -- **Embedding Generation**: Converts knowledge units into high-dimensional vectors for semantic search +See [docs/ARCHITECTURE_FOR_NEW_DOMAINS.md](docs/ARCHITECTURE_FOR_NEW_DOMAINS.md) +for how to extend this stack beyond finance. -- Solution Scenarios: Multiple retrieval patterns including: +#### Governance and loop observability - - **DeepResearch**: Complex multi-hop reasoning across documents - - **RAG**: Retrieval-augmented generation for Q&A - - **Data MCP**: Structured data access protocols - - Custom retrieval patterns based on use case +QuantMind now treats governance as executable policy instead of passive config. +Tool allowlists, loop budgets, fallback behavior, and L3 commit gates are +declared in `quantmind/flows/governance.yaml` and exposed via +`quantmind.flows.governance` helpers (e.g. `ensure_tool_allowed`, +`loop_budget_manager`, `enforce_l3_commit_gates`). Callers opt in by passing +the loaded `GovernancePolicy` (or a default-loaded one) into entry points such +as `magic.resolve_magic_input(..., governance_policy=...)`; the helpers are +not auto-wired into every flow. This maps directly to loop SLI/SLO operations: +loops that opt in are expected to be traceable, budgeted, and quality-gated +before durable writes. --- @@ -107,7 +142,7 @@ We use [uv](https://github.com/astral-sh/uv) for fast and reliable Python packag **Prerequisites:** -- Python 3.8+ +- Python 3.10+ - Git **Installation:** @@ -174,6 +209,28 @@ async def main() -> None: print(paper.model_dump_json(indent=2)) +asyncio.run(main()) +``` + +#### Use the same pipeline on a local memo or technical brief + +```python +import asyncio +from pathlib import Path + +from quantmind.configs import PaperFlowCfg +from quantmind.configs.paper import LocalFilePath +from quantmind.flows import paper_flow + + +async def main() -> None: + doc = await paper_flow( + LocalFilePath(path=Path("docs/internal-research-note.md")), + cfg=PaperFlowCfg(model="gpt-4o-mini"), + ) + print(doc.root.summary) + + asyncio.run(main()) ``` @@ -226,32 +283,61 @@ async def main() -> None: asyncio.run(main()) ``` -> **Note**: QuantMind is mid-migration to OpenAI Agents SDK -> (see [#71](https://github.com/LLMQuant/quant-mind/issues/71)). PR5 lands the -> apex layer (`flows/` + `magic.py`); the remaining work is the `mind/` -> memory + store layer scheduled for PR6 and PR7. +### 🔁 Agentic loops and durable memory + +QuantMind is being tuned for workflows where multiple agents can understand one +another through **shared typed artifacts**, not just prompt conventions. + +Today, that means: + +- resolving loose intent into strict inputs with `magic.resolve_magic_input()` +- extracting typed knowledge with `paper_flow` +- scaling stateless fan-out work with `batch_run()` +- preserving provenance for review and downstream reuse + +Next, that means: + +- filesystem-backed working memory under `mind/memory` +- a store layer for retrieval and longer-lived agent loops +- stronger multi-step patterns for review, refinement, and replay + +If you are building agent teams, think of QuantMind as the layer that provides +stable inputs, stable outputs, and a durable path from observation to memory. --- ### 🗺️ Roadmap -- [x] Better `flow` design for user-friendly usage -- [x] First production level example (Quant Paper Agent) -- [ ] Migrate Agent layer to OpenAI Agents SDK -- [ ] Standardize knowledge format with `knowledge/` (Pydantic-based) -- [ ] Additional content sources (financial news, blogs, reports) -- [ ] Cross-step working memory (`mind/memory`) for batch document processing +- [x] Remove the legacy in-repo agent runtime +- [x] Reposition QuantMind on top of OpenAI Agents SDK +- [x] Land the permanent module roots: `flows/`, `configs/`, `knowledge/`, + `preprocess/`, `magic.py` +- [x] Ship a canonical verification loop (`scripts/verify.sh`) +- [ ] Add filesystem-backed working memory in `mind/memory` +- [ ] Add the store/retrieval layer in `mind/store` +- [ ] Expand beyond the first paper flow with more domain flows +- [ ] Improve multi-agent loop patterns, observability, and replay support +- [ ] Keep agent-facing docs and extension paths under regular review during + active weekly iteration --- ### The Vision: An Intelligent Research Framework > [!IMPORTANT] -> **This section describes our long-term vision, not current capabilities.** While QuantMind today provides a solid knowledge extraction framework, the features described below represent our aspirational goals for future development. +> **This section describes our long-term vision, not current capabilities.** +> QuantMind already ships a useful extraction stack, but the broader memory and +> retrieval story is still under construction. -QuantMind is designed with a larger vision: to become a comprehensive intelligence layer for all financial knowledge. We're building toward a system that understands the interconnections between academic research, market news, analyst reports, and social sentiment—creating a unified knowledge base that powers better financial decisions. +QuantMind is being shaped into a durable intelligence layer for document-heavy +workflows. Finance remains a major proving ground, but the long-term value is +larger: helping AI agents see source material clearly, preserve structured +understanding over time, and operate in loops that do not lose context between +runs. -The foundation we're building today—starting with papers—will expand to encompass the entire financial information ecosystem. +The near-term roadmap starts with papers and research-heavy workflows. The +longer-term goal is a reusable foundation for agentic knowledge work across +domains. > [!NOTE] > **Future Conceptual Example (PR6 brings `FilesystemMemory`):** @@ -267,7 +353,8 @@ The foundation we're building today—starting with papers—will expand to enco > paper: Paper = await paper_flow(ArxivIdentifier(id=arxiv_id), memory=memory) > ``` -This future state represents our commitment to moving beyond simple data aggregation and toward genuine machine intelligence in the financial domain. +This future state represents the shift from one-off extraction to reusable, +memory-aware, agentic knowledge systems. ------ @@ -281,11 +368,11 @@ We welcome contributions of all forms, from bug reports to feature development. **Quick Start for Contributors:** 1. **Fork** the repository -2. **Setup development environment**: +2. **Setup the development environment**: ```bash uv venv && source .venv/bin/activate - uv pip install -e . + uv pip install -e ".[dev]" ./scripts/pre-commit-setup.sh ``` @@ -297,7 +384,7 @@ We welcome contributions of all forms, from bug reports to feature development. - Open an [issue](https://github.com/LLMQuant/quant-mind/issues) to discuss significant changes - Use our issue templates for bug reports and feature requests -- Ensure all pre-commit hooks pass before submitting PR +- Ensure `bash scripts/verify.sh` passes before submitting PR ### License diff --git a/docs/ARCHITECTURE_FOR_NEW_DOMAINS.md b/docs/ARCHITECTURE_FOR_NEW_DOMAINS.md new file mode 100644 index 0000000..ba4f353 --- /dev/null +++ b/docs/ARCHITECTURE_FOR_NEW_DOMAINS.md @@ -0,0 +1,147 @@ +# Extending QuantMind Beyond a Single Domain + +QuantMind currently ships a paper-oriented extraction flow, but the architecture +is meant to support broader agentic knowledge work. This guide explains which +parts are stable, which parts are domain-specific today, and how to extend the +stack without fighting the current design. + +## What is stable today + +The permanent architecture is: + +```text +quantmind/ +├── flows/ # apex orchestration layer +├── knowledge/ # typed knowledge outputs +├── preprocess/ # fetch + format + clean +├── configs/ # typed flow inputs and cfg +├── mind/ # memory/store roadmap +├── magic.py # natural-language resolver +└── utils/ # logger +``` + +These layers already work well for agent teams because they create a strict +contract between source material, extraction, and downstream reuse. + +## What is domain-specific today + +The first production flow is `paper_flow`, and its output schema is +`quantmind.knowledge.Paper`. That flow is optimized for research-paper style +documents and still includes some finance-origin fields such as `asset_classes`. + +That does **not** make the whole repository finance-only. It means the current +reference implementation is paper-first while the underlying architecture is +already reusable: + +- `preprocess/` is domain-agnostic +- `magic.resolve_magic_input()` is flow-agnostic +- `BaseFlowCfg` and `BaseInput` are reusable contracts +- `BaseKnowledge` and the three knowledge shapes are reusable patterns +- `batch_run()` is reusable for stateless fan-out work + +## How to add a new domain + +### 1. Pick the right knowledge shape + +Use: + +- `FlattenKnowledge` for atomic records, events, or summaries +- `TreeKnowledge` for long documents with section/subsection structure +- `GraphKnowledge` only when the relationship layer is actually ready to land + +Start from the retrieval shape you want downstream agents to use, not from the +source format you happen to ingest first. + +### 2. Define typed inputs and configuration + +Add a new config module under `quantmind/configs/`: + +- define one or more `BaseInput` subclasses +- create a discriminated union for the flow input +- extend `BaseFlowCfg` with only domain-relevant knobs + +The goal is for agents and humans to share the same explicit input contract. + +### 3. Implement a pure flow function + +Add a new `async def ..._flow(...)` under `quantmind/flows/` that: + +- accepts one typed input plus `cfg=...` +- fetches or normalizes source content through `preprocess/` +- runs an Agents SDK `Agent(output_type=...)` +- returns a typed knowledge object + +Do not introduce a custom runtime, plugin registry, or class-based flow +hierarchy. + +### 4. Make the flow usable from natural language + +If the flow follows the same `(input, *, cfg, ...)` signature convention, +`magic.resolve_magic_input()` can already resolve free-form intent into typed +input and config objects. + +This is the bridge that helps external agent systems work with QuantMind +without writing brittle prompt parsers. + +## Recommended agentic loop patterns + +### Stateless scout loop + +Use when you want breadth first: + +1. resolve or build many typed inputs +2. run the flow in parallel with `batch_run()` +3. collect typed outputs +4. hand those outputs to a review or ranking agent + +This is the best fit for discovery, triage, and broad corpus scanning. + +### Serial memory loop + +Use when each step depends on previous results: + +1. read one document +2. extract a typed knowledge object +3. update memory or a local run archive +4. feed the next item with that accumulated context + +Today, this pattern should be implemented as an explicit serial loop. `batch_run` +intentionally rejects `memory=` because shared-memory fan-out is not yet the +MVP design. + +### Multi-agent handoff loop + +A simple, durable pattern is: + +1. **Scout agent** resolves intent and gathers sources +2. **Extractor agent** creates typed knowledge objects +3. **Reviewer agent** validates structure, provenance, and confidence +4. **Planner agent** decides what to fetch or revisit next + +The key rule is to pass typed artifacts between agents whenever possible. Avoid +hidden agreements in prompt text when a Pydantic object can carry the contract. + +## Memory and durability roadmap + +The repository is explicitly moving toward a memory-aware architecture: + +- `mind/memory.py` provides the current L1/L2/L3 memory primitives +- `mind/store/` remains the planned retrieval/store layer +- `flows/_runner.py` already reserves the runtime seam for memory integration +- `flows/governance.py` enforces policy for loop budgets, tool allowlists, + fallback behavior, and L3 commit gates + +Until those land, treat QuantMind as a strong typed-extraction layer with a +clear upgrade path toward durable agent loops. + +## Documentation discipline + +QuantMind is increasingly meant to be read by both humans and AI agents. +Because of that: + +- keep `README.md`, `CONTRIBUTING.md`, and this file aligned with code changes +- document new domain assumptions in the same PR that introduces them +- review agent-facing docs regularly during active iteration + +This discipline matters because stale documentation causes broken agent loops +just as quickly as stale code. diff --git a/docs/fincept_integration.md b/docs/fincept_integration.md new file mode 100644 index 0000000..2e875b3 --- /dev/null +++ b/docs/fincept_integration.md @@ -0,0 +1,72 @@ +# Fincept ↔ QuantMind Integration Guide + +## Overview + +QuantMind serves as the **knowledge extraction layer** for Fincept AI Ops. +Rather than raw market data, Fincept can call QuantMind to: + +- Distill research insights from financial papers +- Produce structured signal metadata (alpha factors, risk signals) +- Answer natural-language questions over quantitative finance literature + +## Integration Architecture + +QuantMind is consumed from Python (no dedicated HTTP service ships in this +repo). Fincept can call QuantMind flows directly from its worker process, or +wrap them in a thin internal endpoint if a network boundary is required. + +``` +fincept-ai-ops (worker / FastAPI endpoint) + | + | from quantmind.flows import paper_flow + | doc = await paper_flow(LocalFilePath(...), cfg=PaperFlowCfg(...)) + v +QuantMind Flow (paper_flow) + | + v +Extracted Insights JSON (typed Pydantic + provenance) +``` + +## Usage Example + +```python +import asyncio +from pathlib import Path + +from quantmind.configs import PaperFlowCfg +from quantmind.configs.paper import LocalFilePath +from quantmind.flows import paper_flow + + +async def main() -> None: + doc = await paper_flow( + LocalFilePath(path=Path("path/to/document.pdf")), + cfg=PaperFlowCfg(model="gpt-4o-mini"), + ) + # Persist / index this JSON in Fincept's storage layer. + print(doc.model_dump(mode="json")) + + +asyncio.run(main()) +``` + +## Synergy with Fincept + +| Fincept Component | QuantMind Feature | +|--------------------------|--------------------------------------------| +| Strategy generation | Research paper extraction | +| Risk model inputs | Factor library | +| Audit log enrichment | Source citation tracking | +| Backtest hypothesis | Literature-validated signals | + +## Setup + +1. Install QuantMind alongside Fincept in the same Python environment + (`pip install -e .` from this repository). +2. From Fincept, import the flow you need (for example `paper_flow`) and + call it as shown in the usage example. No `quantmind serve` CLI ships + with this repo; if Fincept needs a network boundary, expose `paper_flow` + (or any other flow) through your own thin FastAPI wrapper. + +For broader context on the Fincept integration milestone, see the Fincept +project's own roadmap in the Fincept repository. diff --git a/draft_corrections.md b/draft_corrections.md new file mode 100644 index 0000000..d4ca917 --- /dev/null +++ b/draft_corrections.md @@ -0,0 +1,58 @@ +# Draft Corrections: Verified Memory Repo Shortlist + +## 1) Dogrulama olcutu + +Bu calisma "en iyi" iddiasi degil, "dogrulanmis adaylar" uretir. Bir repo shortlist'e girmek icin asagidaki kosullari birlikte saglamalidir: + +1. Son 90 gunde aktif commit hareketi var. +2. Gercek bir memory architecture sunuyor (tier, retrieval, persistence, governance gibi somut katmanlar). +3. Agentic AI veya autonomous agent akislarina dogrudan temas ediyor. +4. Acik dokumantasyon ve calisan ornek kullanim veriyor. +5. Yildiz/fork/adoption sinyali ile topluluk ilgisi goruluyor. + +Degerlendirme notu: +- Stars tek basina karar kriteri degildir. +- Mimari netlik + bakim aktivitesi + entegrasyon kaniti birincil agirliktir. + +## 2) Haric tutma kurallari + +Asagidaki repo tipleri shortlist disinda tutulur: + +1. Sadece repo adi veya README icinde "memory" geciyor, fakat gercek memory altyapisi yok. +2. Genel framework sunuyor, ancak memory disiplini (state lifecycle, retrieval policy, persistence strategy) icermiyor. +3. Eski, pasif, bakim disi veya son donemde anlamli gelistirme gostermeyen projeler. +4. Mimariyi dogrulayan kanitlar eksik (zayif docs, belirsiz API, calismayan ornekler). + +## 3) QuantMind icin cikarim + +### Copilot Memory benzeri patternler + +- Citation-backed facts +- Validation before use +- Stale item cleanup +- Repo-scoped memory + +### QuantMind'e dogrudan uyarlama + +- `RawFallbackNode`: Yapilandirilamayan veya dusuk guvenli ciktilari kaybetmeden ham iz olarak saklama. +- `QualityGate`: Durable katmana commit oncesi sema, kaynak ve tutarlilik denetimi. +- `Hybrid memory tiers`: Working, episodic ve durable katmanlari amaca gore ayirma. +- `Graph only for distilled facts`: Graph katmanina yalnizca rafine edilmis, kaynaklanmis ve tekrar dogrulanmis bilgi yazma. + +### L3 commit icin zorunlu kosullar + +`confidence >= 0.85` tek basina yeterli degildir. Asagidaki kosullar birlikte zorunludur: + +1. Provenance mevcut ve dogrulanabilir. +2. Schema validity basarili. +3. Dedup ve contradiction kontrolu basarili. + +## Sonraki adim (uygulama hazirligi) + +Bu taslak sonraki iterasyonda su ciktiya donusturulmelidir: + +- Dogrulanmis 21 repo shortlist +- Her repo icin memory yaklasimi siniflandirmasi +- QuantMind uyarlanabilirlik puani +- Risk/avantaj analizi +- Kopyalanacak mimari pattern oneri matrisi diff --git a/examples/mind/recoverable_memory_demo.py b/examples/mind/recoverable_memory_demo.py new file mode 100644 index 0000000..55ba939 --- /dev/null +++ b/examples/mind/recoverable_memory_demo.py @@ -0,0 +1,54 @@ +"""Minimal demo for recoverable ingestion and gated memory commit.""" + +from tempfile import TemporaryDirectory + +from pydantic import BaseModel, ConfigDict + +from quantmind.configs.recoverable import RecoverableValidation +from quantmind.flows.governance import load_governance_policy +from quantmind.mind.memory import HybridMemoryEngine + + +class DemoArtifact(BaseModel): + """Strict schema that rejects unexpected fields.""" + + model_config = ConfigDict(extra="forbid") + id: str + title: str + validation_confidence: float + provenance: dict[str, str] + + +def main() -> None: + validator = RecoverableValidation(DemoArtifact) + policy = load_governance_policy() + payload = { + "id": "artifact-1", + "title": "Recovered architecture summary", + "validation_confidence": 0.91, + "provenance": {"source": "demo"}, + "unexpected_field": "will trigger fallback", + } + + with TemporaryDirectory() as tmpdir: + memory = HybridMemoryEngine(base_path=tmpdir) + parsed = validator.execute_safely(payload) + memory.write_l1_trace("Parser", parsed.model_dump(mode="json")) + + if isinstance(parsed, DemoArtifact): + committed = memory.commit_to_l3_graph( + parsed.model_dump(mode="json"), + schema_valid=True, + dedup_ok=True, + contradiction_free=True, + ) + print("Committed to L3:", committed) + else: + fallback_node = parsed + fallback = policy.global_settings.fallback_policy + print("Fallback policy:", fallback) + print("Quarantined schema:", fallback_node.target_schema) + + +if __name__ == "__main__": + main() diff --git a/quantmind/__init__.py b/quantmind/__init__.py index 91edcda..02dcb2c 100644 --- a/quantmind/__init__.py +++ b/quantmind/__init__.py @@ -1,7 +1,7 @@ -"""QuantMind: Intelligent Knowledge Extraction and Retrieval Framework. +"""QuantMind: Intelligent knowledge extraction for agentic workflows. -QuantMind transforms unstructured financial content into a queryable knowledge graph -through a two-stage architecture focused on knowledge extraction and intelligent retrieval. +QuantMind transforms unstructured documents into typed, provenance-aware +artifacts that can be validated, governed, and reused in durable loops. """ __version__ = "0.0.1" diff --git a/quantmind/configs/__init__.py b/quantmind/configs/__init__.py index 271be37..eb88e6d 100644 --- a/quantmind/configs/__init__.py +++ b/quantmind/configs/__init__.py @@ -12,6 +12,7 @@ from quantmind.configs.earnings import EarningsFlowCfg, EarningsInput from quantmind.configs.news import NewsFlowCfg, NewsInput from quantmind.configs.paper import PaperFlowCfg, PaperInput +from quantmind.configs.recoverable import RawFallbackNode, RecoverableValidation __all__ = [ "BaseFlowCfg", @@ -22,4 +23,6 @@ "NewsInput", "PaperFlowCfg", "PaperInput", + "RawFallbackNode", + "RecoverableValidation", ] diff --git a/quantmind/configs/recoverable.py b/quantmind/configs/recoverable.py new file mode 100644 index 0000000..dd4909b --- /dev/null +++ b/quantmind/configs/recoverable.py @@ -0,0 +1,42 @@ +"""Recoverable parsing helpers for tolerant ingestion boundaries. + +This module keeps strict schema contracts in place (`extra="forbid"` on +the target model) while preventing pipeline paralysis during malformed +inputs. Callers receive either a validated model or a structured +quarantine node that preserves raw context. +""" + +from typing import Any, Generic, TypeVar + +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +T = TypeVar("T", bound=BaseModel) + + +class RawFallbackNode(BaseModel): + """Quarantine payload used when schema validation fails.""" + + model_config = ConfigDict(extra="forbid") + + raw_payload: dict[str, Any] = Field(default_factory=dict) + target_schema: str + error_message: str + context_loss_prevented: bool = True + + +class RecoverableValidation(Generic[T]): + """Validate against a target model and quarantine failures.""" + + def __init__(self, target_model: type[T]) -> None: + self.target_model = target_model + + def execute_safely(self, data: dict[str, Any]) -> T | RawFallbackNode: + """Return validated model or a fallback node with original payload.""" + try: + return self.target_model.model_validate(data) + except ValidationError as error: + return RawFallbackNode( + raw_payload=data, + target_schema=self.target_model.__name__, + error_message=str(error), + ) diff --git a/quantmind/flows/__init__.py b/quantmind/flows/__init__.py index 78dd9bf..839f9c9 100644 --- a/quantmind/flows/__init__.py +++ b/quantmind/flows/__init__.py @@ -1,7 +1,10 @@ """Apex layer — composes configs / knowledge / preprocess on the SDK. -Each flow function (``paper_flow``, future ``news_flow`` / ``earnings_flow``) -takes a typed input and a ``FlowCfg`` and returns a knowledge item. +Each flow function takes a typed input and a ``FlowCfg`` and returns a +knowledge item. The current production flow is paper-oriented (``paper_flow``), +but the apex-layer contract itself is reusable for future domain flows as long +as they follow the same typed ``(input, *, cfg, ...)`` pattern. + Cross-flow utilities live alongside: - ``batch_run`` runs any flow over a list of inputs with bounded @@ -12,11 +15,29 @@ """ from quantmind.flows.batch import BatchResult, batch_run +from quantmind.flows.governance import ( + GovernancePolicy, + GovernancePolicyError, + LoopBudgetManager, + enforce_l3_commit_gates, + ensure_tool_allowed, + load_governance_policy, + loop_budget_manager, + run_fallback_policy, +) from quantmind.flows.paper import UnsupportedContentTypeError, paper_flow __all__ = [ "BatchResult", + "GovernancePolicy", + "GovernancePolicyError", + "LoopBudgetManager", "UnsupportedContentTypeError", "batch_run", + "enforce_l3_commit_gates", + "ensure_tool_allowed", + "load_governance_policy", + "loop_budget_manager", "paper_flow", + "run_fallback_policy", ] diff --git a/quantmind/flows/governance.py b/quantmind/flows/governance.py new file mode 100644 index 0000000..6908742 --- /dev/null +++ b/quantmind/flows/governance.py @@ -0,0 +1,225 @@ +"""Governance policy loader and runtime enforcement helpers.""" + +from pathlib import Path +from typing import Any, Literal + +import yaml +from pydantic import BaseModel, ConfigDict, Field, ValidationError + +from quantmind.configs import RawFallbackNode +from quantmind.mind.memory import L3CommitRequirements, can_commit_to_l3 + + +class GovernancePolicyError(ValueError): + """Raised when governance policy content cannot be loaded or validated.""" + + +class AgentGovernanceRule(BaseModel): + """Tool + tier constraints for one role in a scenario.""" + + model_config = ConfigDict(extra="forbid") + + role: str + allowed_tools: list[str] = Field(default_factory=list) + output_tier: Literal["L1", "L2", "L3"] + required_schema: str | None = None + confidence_threshold: float | None = None + fallback_node: str | None = None + + +class ScenarioGovernanceRule(BaseModel): + """Scenario-level set of role policies.""" + + model_config = ConfigDict(extra="forbid") + + description: str + agents: list[AgentGovernanceRule] = Field(default_factory=list) + + +class GlobalGovernanceSettings(BaseModel): + """Global runtime limits and failure behavior.""" + + model_config = ConfigDict(extra="forbid") + + loop_budget_max: int = 5 + fallback_policy: Literal["quarantine_and_continue", "fail_fast"] = ( + "quarantine_and_continue" + ) + + +class L3CommitPolicy(BaseModel): + """Hard gates required before committing to durable memory. + + Mirrors :class:`quantmind.mind.memory.L3CommitRequirements` field for + field so that :func:`enforce_l3_commit_gates` can delegate to the shared + ``can_commit_to_l3`` helper. Keep these defaults in sync. + """ + + model_config = ConfigDict(extra="forbid") + + min_confidence: float = 0.85 + require_provenance: bool = True + require_schema_validity: bool = True + require_dedup_check: bool = True + require_contradiction_check: bool = True + + def to_requirements(self) -> L3CommitRequirements: + """Convert to the dataclass used by ``mind.memory``.""" + return L3CommitRequirements( + min_confidence=self.min_confidence, + require_provenance=self.require_provenance, + require_schema_validity=self.require_schema_validity, + require_dedup_check=self.require_dedup_check, + require_contradiction_check=self.require_contradiction_check, + ) + + +class GovernancePolicy(BaseModel): + """Full governance policy document.""" + + model_config = ConfigDict(extra="forbid") + + version: str + global_settings: GlobalGovernanceSettings = Field( + default_factory=GlobalGovernanceSettings + ) + l3_commit: L3CommitPolicy = Field(default_factory=L3CommitPolicy) + scenarios: dict[str, ScenarioGovernanceRule] = Field(default_factory=dict) + + def role_rule( + self, scenario_name: str, role: str + ) -> AgentGovernanceRule: + """Resolve the role rule for one scenario.""" + try: + scenario = self.scenarios[scenario_name] + except KeyError as error: + raise KeyError( + f"Unknown governance scenario: {scenario_name!r}" + ) from error + for rule in scenario.agents: + if rule.role == role: + return rule + raise KeyError( + f"Role {role!r} is not defined in scenario {scenario_name!r}" + ) + + +def load_governance_policy(path: str | Path | None = None) -> GovernancePolicy: + """Load and validate governance policy YAML.""" + policy_path = Path(path) if path is not None else _default_policy_path() + try: + loaded = yaml.safe_load(policy_path.read_text(encoding="utf-8")) + except OSError as error: + raise GovernancePolicyError( + f"Unable to read governance policy: {policy_path}" + ) from error + except yaml.YAMLError as error: + raise GovernancePolicyError( + f"Invalid YAML in governance policy: {policy_path}" + ) from error + + if loaded is None: + raise GovernancePolicyError( + f"Governance policy is empty: {policy_path}" + ) + try: + return GovernancePolicy.model_validate(loaded) + except ValidationError as error: + raise GovernancePolicyError( + f"Governance policy schema validation failed: {policy_path}" + ) from error + + +def ensure_tool_allowed( + policy: GovernancePolicy, + *, + scenario_name: str, + role: str, + tool_name: str, +) -> None: + """Raise when a role tries to use a tool outside its allowlist.""" + rule = policy.role_rule(scenario_name, role) + if tool_name not in rule.allowed_tools: + raise PermissionError( + f"Tool {tool_name!r} is not allowed for role {role!r} " + f"in scenario {scenario_name!r}" + ) + + +class LoopBudgetManager: + """Track and enforce max loop budget from governance policy.""" + + def __init__(self, max_loops: int) -> None: + if max_loops <= 0: + raise ValueError("max_loops must be positive") + self.max_loops = max_loops + self._consumed = 0 + + @property + def remaining(self) -> int: + """Return remaining budget steps.""" + return self.max_loops - self._consumed + + def consume(self, *, steps: int = 1) -> int: + """Consume loop budget and return the remaining amount.""" + if steps <= 0: + raise ValueError("steps must be positive") + if self._consumed + steps > self.max_loops: + raise RuntimeError( + f"Loop budget exceeded: requested {steps}, " + f"remaining {self.remaining}" + ) + self._consumed += steps + return self.remaining + + +def loop_budget_manager(policy: GovernancePolicy) -> LoopBudgetManager: + """Create a budget manager from policy defaults.""" + return LoopBudgetManager(policy.global_settings.loop_budget_max) + + +def run_fallback_policy( + policy: GovernancePolicy, + *, + target_schema: str, + payload: dict[str, Any], + error_message: str, +) -> RawFallbackNode: + """Run fallback behavior configured by governance policy.""" + fallback = policy.global_settings.fallback_policy + if fallback == "quarantine_and_continue": + return RawFallbackNode( + raw_payload=payload, + target_schema=target_schema, + error_message=error_message, + ) + raise RuntimeError( + f"Fallback policy {fallback!r} blocks continuation" + ) + + +def enforce_l3_commit_gates( + policy: GovernancePolicy, + *, + artifact: dict[str, Any], + schema_valid: bool, + dedup_ok: bool, + contradiction_free: bool, +) -> bool: + """Return true only when all configured L3 commit gates pass. + + Delegates to :func:`quantmind.mind.memory.can_commit_to_l3` so there is + a single source of truth for L3 commit gating semantics across flows + and the mind/memory engine. + """ + return can_commit_to_l3( + artifact, + schema_valid=schema_valid, + dedup_ok=dedup_ok, + contradiction_free=contradiction_free, + requirements=policy.l3_commit.to_requirements(), + ) + + +def _default_policy_path() -> Path: + return Path(__file__).with_name("governance.yaml") diff --git a/quantmind/flows/governance.yaml b/quantmind/flows/governance.yaml new file mode 100644 index 0000000..f816c53 --- /dev/null +++ b/quantmind/flows/governance.yaml @@ -0,0 +1,43 @@ +version: "2.0.0" + +global_settings: + loop_budget_max: 5 + fallback_policy: "quarantine_and_continue" + +l3_commit: + min_confidence: 0.85 + require_provenance: true + require_schema_validity: true + require_dedup_check: true + require_contradiction_check: true + +scenarios: + architecture_analysis: + description: "Technical document analysis and refactor suggestion" + agents: + - role: "Scout" + allowed_tools: ["scan_repo", "read_file"] + output_tier: "L1" + - role: "Extractor" + allowed_tools: ["parse_ast"] + required_schema: "ArchitectureSummary" + output_tier: "L2" + - role: "Reviewer" + allowed_tools: ["diff_analyze"] + confidence_threshold: 0.8 + output_tier: "L3" + + tolerant_ingestion: + description: "Ingesting malformed multi-source datasets safely" + agents: + - role: "Scout" + allowed_tools: ["fetch_source"] + output_tier: "L1" + - role: "Parser" + allowed_tools: ["recoverable_validate"] + fallback_node: "RawFallbackNode" + output_tier: "L2" + - role: "GraphWriter" + allowed_tools: ["graph_write"] + confidence_threshold: 0.9 + output_tier: "L3" diff --git a/quantmind/magic.py b/quantmind/magic.py index 71e522a..5c44b96 100644 --- a/quantmind/magic.py +++ b/quantmind/magic.py @@ -23,6 +23,7 @@ from pydantic import BaseModel from quantmind.configs.base import BaseFlowCfg +from quantmind.flows.governance import GovernancePolicy, ensure_tool_allowed InputT = TypeVar("InputT", bound=BaseModel) CfgT = TypeVar("CfgT", bound=BaseModel) @@ -65,6 +66,10 @@ async def resolve_magic_input( target_flow: Callable[..., Awaitable[Any]], resolver_model: str = "gpt-4o-mini", resolver_instructions: str | None = None, + governance_policy: GovernancePolicy | None = None, + governance_scenario: str | None = None, + requested_tools: list[str] | None = None, + resolver_role: str = "Resolver", ) -> tuple[Any, Any]: """Parse ``natural_language`` into ``(input_obj, cfg_obj)`` for ``target_flow``. @@ -76,11 +81,24 @@ async def resolve_magic_input( resolver_instructions: Optional override for the resolver's system prompt template. Receives ``flow_name``, ``input_schema``, and ``cfg_schema`` via ``str.format``. + governance_policy: Optional governance policy to enforce before + model execution. + governance_scenario: Scenario name used with + ``governance_policy`` for tool allowlist checks. + requested_tools: Tool names that the resolver intends to use. + resolver_role: Role name used for governance lookup when policy + checks are enabled. Returns: Tuple of ``(input_obj, cfg_obj)`` populated by the resolver. """ input_type, cfg_type = _introspect_flow_signature(target_flow) + _enforce_governance( + governance_policy=governance_policy, + governance_scenario=governance_scenario, + requested_tools=requested_tools, + resolver_role=resolver_role, + ) template = resolver_instructions or _RESOLVER_INSTRUCTIONS instructions = template.format( flow_name=target_flow.__name__, @@ -103,12 +121,20 @@ async def preview_resolve( *, target_flow: Callable[..., Awaitable[Any]], resolver_model: str = "gpt-4o-mini", + governance_policy: GovernancePolicy | None = None, + governance_scenario: str | None = None, + requested_tools: list[str] | None = None, + resolver_role: str = "Resolver", ) -> tuple[Any, Any]: """Resolve and pretty-print the result without invoking the flow.""" inp, cfg = await resolve_magic_input( natural_language, target_flow=target_flow, resolver_model=resolver_model, + governance_policy=governance_policy, + governance_scenario=governance_scenario, + requested_tools=requested_tools, + resolver_role=resolver_role, ) print("input_obj:", inp.model_dump_json(indent=2)) print("cfg_obj:", cfg.model_dump_json(indent=2)) @@ -199,3 +225,26 @@ def _pydantic_schema_str(t: Any) -> str: ] return json.dumps({"oneOf": schemas}, indent=2) return repr(t) + + +def _enforce_governance( + *, + governance_policy: GovernancePolicy | None, + governance_scenario: str | None, + requested_tools: list[str] | None, + resolver_role: str, +) -> None: + """Apply optional resolver-side governance checks.""" + if governance_policy is None: + return + if not governance_scenario: + raise ValueError( + "governance_scenario is required when governance_policy is set" + ) + for tool_name in requested_tools or []: + ensure_tool_allowed( + governance_policy, + scenario_name=governance_scenario, + role=resolver_role, + tool_name=tool_name, + ) diff --git a/quantmind/mind/__init__.py b/quantmind/mind/__init__.py new file mode 100644 index 0000000..331bd38 --- /dev/null +++ b/quantmind/mind/__init__.py @@ -0,0 +1,9 @@ +"""Mind layer primitives (memory/storage surfaces).""" + +from quantmind.mind.memory import ( + L3CommitRequirements, + HybridMemoryEngine, + can_commit_to_l3, +) + +__all__ = ["HybridMemoryEngine", "L3CommitRequirements", "can_commit_to_l3"] diff --git a/quantmind/mind/memory.py b/quantmind/mind/memory.py new file mode 100644 index 0000000..9e787b7 --- /dev/null +++ b/quantmind/mind/memory.py @@ -0,0 +1,95 @@ +"""Hybrid memory primitives for L1/L2/L3 data handling.""" + +import json +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +@dataclass(frozen=True) +class L3CommitRequirements: + """Hard gates required before durable L3 commit.""" + + min_confidence: float = 0.85 + require_provenance: bool = True + require_schema_validity: bool = True + require_dedup_check: bool = True + require_contradiction_check: bool = True + + +def can_commit_to_l3( + artifact: dict[str, Any], + *, + schema_valid: bool, + dedup_ok: bool, + contradiction_free: bool, + requirements: L3CommitRequirements | None = None, +) -> bool: + """Return true only when artifact passes all required L3 gates.""" + req = requirements or L3CommitRequirements() + confidence = float(artifact.get("validation_confidence", 0.0)) + if confidence < req.min_confidence: + return False + if req.require_provenance and not artifact.get("provenance"): + return False + if req.require_schema_validity and not schema_valid: + return False + if req.require_dedup_check and not dedup_ok: + return False + if req.require_contradiction_check and not contradiction_free: + return False + return True + + +class HybridMemoryEngine: + """L1 (transient) -> L2 (working) -> L3 (durable) memory manager.""" + + def __init__(self, base_path: str = "~/.quantmind/mind") -> None: + self.base_path = Path(base_path).expanduser() + self.base_path.mkdir(parents=True, exist_ok=True) + self.l1_path = self.base_path / "l1_ephemeral.jsonl" + self.l2_path = self.base_path / "l2_working.json" + self.l3_path = self.base_path / "l3_durable.jsonl" + + def write_l1_trace(self, agent_role: str, tool_output: Any) -> None: + """Append raw tool output to L1 trace.""" + record = {"role": agent_role, "payload": tool_output} + with self.l1_path.open("a", encoding="utf-8") as file: + file.write(json.dumps(record, ensure_ascii=False) + "\n") + + def update_l2_state(self, key: str, value: Any) -> dict[str, Any]: + """Update and persist L2 working state as JSON.""" + current: dict[str, Any] = {} + if self.l2_path.exists(): + with self.l2_path.open("r", encoding="utf-8") as file: + loaded = json.load(file) + if isinstance(loaded, dict): + current = loaded + current[key] = value + with self.l2_path.open("w", encoding="utf-8") as file: + json.dump(current, file, ensure_ascii=False, indent=2, sort_keys=True) + return current + + def commit_to_l3_graph( + self, + validated_artifact: dict[str, Any], + *, + schema_valid: bool, + dedup_ok: bool, + contradiction_free: bool, + requirements: L3CommitRequirements | None = None, + ) -> bool: + """Commit artifact to L3 only when every hard gate passes.""" + if not can_commit_to_l3( + validated_artifact, + schema_valid=schema_valid, + dedup_ok=dedup_ok, + contradiction_free=contradiction_free, + requirements=requirements, + ): + return False + with self.l3_path.open("a", encoding="utf-8") as file: + file.write( + json.dumps(validated_artifact, ensure_ascii=False) + "\n" + ) + return True diff --git a/tests/configs/test_recoverable.py b/tests/configs/test_recoverable.py new file mode 100644 index 0000000..86f905e --- /dev/null +++ b/tests/configs/test_recoverable.py @@ -0,0 +1,44 @@ +"""Tests for recoverable validation helpers.""" + +import unittest + +from pydantic import BaseModel, ConfigDict + +from quantmind.configs.recoverable import RawFallbackNode, RecoverableValidation + + +class _StrictSample(BaseModel): + model_config = ConfigDict(extra="forbid") + name: str + score: float + + +class RecoverableValidationTests(unittest.TestCase): + def test_returns_validated_model_on_success(self) -> None: + validator = RecoverableValidation(_StrictSample) + out = validator.execute_safely({"name": "node-1", "score": 0.91}) + self.assertIsInstance(out, _StrictSample) + assert isinstance(out, _StrictSample) + self.assertEqual(out.name, "node-1") + self.assertEqual(out.score, 0.91) + + def test_returns_fallback_node_on_validation_error(self) -> None: + validator = RecoverableValidation(_StrictSample) + out = validator.execute_safely( + {"name": "node-1", "score": 0.91, "unexpected": "x"} + ) + self.assertIsInstance(out, RawFallbackNode) + assert isinstance(out, RawFallbackNode) + self.assertEqual(out.target_schema, "_StrictSample") + self.assertTrue(out.context_loss_prevented) + self.assertEqual(out.raw_payload["unexpected"], "x") + # Assert on a stable substring that Pydantic consistently emits across + # versions/locales for this kind of extra-field error, rather than on + # the full human-readable message text (which can change between + # Pydantic releases). + self.assertIn("unexpected", out.error_message) + self.assertTrue(out.error_message) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/flows/test_governance.py b/tests/flows/test_governance.py new file mode 100644 index 0000000..68aac48 --- /dev/null +++ b/tests/flows/test_governance.py @@ -0,0 +1,139 @@ +"""Tests for ``quantmind.flows.governance``.""" + +import tempfile +import unittest +from pathlib import Path + +from quantmind.flows.governance import ( + GovernancePolicy, + GovernancePolicyError, + enforce_l3_commit_gates, + ensure_tool_allowed, + load_governance_policy, + loop_budget_manager, + run_fallback_policy, +) + + +class GovernanceLoaderTests(unittest.TestCase): + def test_load_default_policy(self) -> None: + policy = load_governance_policy() + self.assertEqual(policy.version, "2.0.0") + self.assertEqual(policy.global_settings.loop_budget_max, 5) + self.assertIn("tolerant_ingestion", policy.scenarios) + + def test_load_empty_policy_raises(self) -> None: + # Use a temp directory instead of writing into the repo's tests/ + # tree; keeps the test safe in read-only envs and under parallel + # test runs. + with tempfile.TemporaryDirectory() as tmp_dir: + empty_path = Path(tmp_dir) / "empty_governance.yaml" + empty_path.write_text("", encoding="utf-8") + with self.assertRaises(GovernancePolicyError): + load_governance_policy(empty_path) + + +class ToolAllowlistTests(unittest.TestCase): + def setUp(self) -> None: + self.policy = load_governance_policy() + + def test_allowed_tool_passes(self) -> None: + ensure_tool_allowed( + self.policy, + scenario_name="architecture_analysis", + role="Scout", + tool_name="scan_repo", + ) + + def test_disallowed_tool_raises(self) -> None: + with self.assertRaises(PermissionError): + ensure_tool_allowed( + self.policy, + scenario_name="architecture_analysis", + role="Scout", + tool_name="graph_write", + ) + + +class LoopBudgetTests(unittest.TestCase): + def setUp(self) -> None: + self.policy = load_governance_policy() + + def test_budget_consumption(self) -> None: + manager = loop_budget_manager(self.policy) + self.assertEqual(manager.remaining, 5) + self.assertEqual(manager.consume(), 4) + self.assertEqual(manager.consume(steps=2), 2) + + def test_budget_overflow_raises(self) -> None: + manager = loop_budget_manager(self.policy) + manager.consume(steps=5) + with self.assertRaises(RuntimeError): + manager.consume() + + +class FallbackPolicyTests(unittest.TestCase): + def test_quarantine_fallback_returns_node(self) -> None: + policy = load_governance_policy() + node = run_fallback_policy( + policy, + target_schema="ExampleSchema", + payload={"x": 1}, + error_message="invalid", + ) + self.assertEqual(node.target_schema, "ExampleSchema") + self.assertEqual(node.raw_payload["x"], 1) + + def test_fail_fast_policy_raises(self) -> None: + policy = GovernancePolicy.model_validate( + { + "version": "2.0.0", + "global_settings": { + "loop_budget_max": 1, + "fallback_policy": "fail_fast", + }, + "scenarios": {}, + } + ) + with self.assertRaises(RuntimeError): + run_fallback_policy( + policy, + target_schema="ExampleSchema", + payload={}, + error_message="x", + ) + + +class L3CommitGateTests(unittest.TestCase): + def setUp(self) -> None: + self.policy = load_governance_policy() + + def test_accepts_when_all_gates_pass(self) -> None: + ok = enforce_l3_commit_gates( + self.policy, + artifact={ + "validation_confidence": 0.9, + "provenance": {"source": "test"}, + }, + schema_valid=True, + dedup_ok=True, + contradiction_free=True, + ) + self.assertTrue(ok) + + def test_rejects_when_any_gate_fails(self) -> None: + rejected = enforce_l3_commit_gates( + self.policy, + artifact={ + "validation_confidence": 0.9, + "provenance": {"source": "test"}, + }, + schema_valid=True, + dedup_ok=False, + contradiction_free=True, + ) + self.assertFalse(rejected) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/mind/__init__.py b/tests/mind/__init__.py new file mode 100644 index 0000000..ea6dad5 --- /dev/null +++ b/tests/mind/__init__.py @@ -0,0 +1 @@ +"""Tests for quantmind.mind package.""" diff --git a/tests/mind/test_memory.py b/tests/mind/test_memory.py new file mode 100644 index 0000000..db5d981 --- /dev/null +++ b/tests/mind/test_memory.py @@ -0,0 +1,84 @@ +"""Tests for ``quantmind.mind.memory``.""" + +import json +import tempfile +import unittest + +from quantmind.mind.memory import HybridMemoryEngine, can_commit_to_l3 + + +class HybridMemoryEngineTests(unittest.TestCase): + def test_write_l1_trace_appends_jsonl(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + engine = HybridMemoryEngine(base_path=tmpdir) + engine.write_l1_trace("Scout", {"tool": "scan_repo", "ok": True}) + content = engine.l1_path.read_text(encoding="utf-8").strip() + parsed = json.loads(content) + self.assertEqual(parsed["role"], "Scout") + self.assertTrue(parsed["payload"]["ok"]) + + def test_update_l2_state_round_trip(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + engine = HybridMemoryEngine(base_path=tmpdir) + current = engine.update_l2_state("step", {"status": "running"}) + self.assertEqual(current["step"]["status"], "running") + persisted = json.loads(engine.l2_path.read_text(encoding="utf-8")) + self.assertEqual(persisted["step"]["status"], "running") + + def test_commit_to_l3_requires_all_gates(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + engine = HybridMemoryEngine(base_path=tmpdir) + accepted = engine.commit_to_l3_graph( + { + "id": "artifact-1", + "validation_confidence": 0.9, + "provenance": {"source": "test"}, + }, + schema_valid=True, + dedup_ok=True, + contradiction_free=True, + ) + rejected = engine.commit_to_l3_graph( + { + "id": "artifact-2", + "validation_confidence": 0.9, + "provenance": {"source": "test"}, + }, + schema_valid=True, + dedup_ok=False, + contradiction_free=True, + ) + self.assertTrue(accepted) + self.assertFalse(rejected) + lines = engine.l3_path.read_text(encoding="utf-8").strip().splitlines() + self.assertEqual(len(lines), 1) + self.assertEqual(json.loads(lines[0])["id"], "artifact-1") + + +class CanCommitToL3Tests(unittest.TestCase): + def test_rejects_without_provenance(self) -> None: + self.assertFalse( + can_commit_to_l3( + {"validation_confidence": 0.9}, + schema_valid=True, + dedup_ok=True, + contradiction_free=True, + ) + ) + + def test_rejects_low_confidence(self) -> None: + self.assertFalse( + can_commit_to_l3( + { + "validation_confidence": 0.5, + "provenance": {"source": "test"}, + }, + schema_valid=True, + dedup_ok=True, + contradiction_free=True, + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_magic.py b/tests/test_magic.py index 426f6cd..f8b03e9 100644 --- a/tests/test_magic.py +++ b/tests/test_magic.py @@ -12,8 +12,10 @@ from quantmind.configs import PaperFlowCfg from quantmind.configs.paper import ArxivIdentifier, PaperInput from quantmind.flows import paper_flow +from quantmind.flows.governance import load_governance_policy from quantmind.magic import ( ResolvedFlowConfig, + _enforce_governance, _introspect_flow_signature, _pydantic_schema_str, _strip_optional, @@ -196,6 +198,37 @@ def _capture_agent(*_a: object, **kwargs: object) -> object: self.assertEqual(captured["model"], "claude-3-5-sonnet") +class GovernanceIntegrationTests(unittest.TestCase): + def setUp(self) -> None: + self.policy = load_governance_policy() + + def test_policy_requires_scenario(self) -> None: + with self.assertRaises(ValueError): + _enforce_governance( + governance_policy=self.policy, + governance_scenario=None, + requested_tools=["scan_repo"], + resolver_role="Scout", + ) + + def test_disallowed_tool_raises_permission_error(self) -> None: + with self.assertRaises(PermissionError): + _enforce_governance( + governance_policy=self.policy, + governance_scenario="architecture_analysis", + requested_tools=["graph_write"], + resolver_role="Scout", + ) + + def test_allowed_tools_pass(self) -> None: + _enforce_governance( + governance_policy=self.policy, + governance_scenario="architecture_analysis", + requested_tools=["scan_repo", "read_file"], + resolver_role="Scout", + ) + + class PreviewResolveTests(unittest.IsolatedAsyncioTestCase): async def test_prints_and_returns_tuple(self) -> None: resolved = ResolvedFlowConfig[PaperInput, PaperFlowCfg](