diff --git a/.gitignore b/.gitignore index 539aa6c..ac90035 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ CMakeLists_modified.txt build/ *.egg-info +.venv/ lib/ bin/ diff --git a/.impeccable/hook.cache.json b/.impeccable/hook.cache.json new file mode 100644 index 0000000..ebc7c5d --- /dev/null +++ b/.impeccable/hook.cache.json @@ -0,0 +1 @@ +{"version":1,"sessions":{}} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 014f892..736deef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,144 +1,144 @@ # Contributing to QuantMind -Thank you for contributing to QuantMind! This guide provides essential information for developers. +Thank you for contributing to QuantMind. -## ๐Ÿš€ 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**: - ```bash - uv venv && source .venv/bin/activate - uv pip install -e . - ``` -3. **Install pre-commit hooks**: - ```bash - ./scripts/pre-commit-setup.sh - ``` +## ๐Ÿš€ Quick setup -## ๐Ÿ› ๏ธ Development Setup +```bash +uv venv +source .venv/bin/activate +uv pip install -e ".[dev]" +./scripts/pre-commit-setup.sh +``` -### Pre-commit Hooks +If `uv` is unavailable in your environment, a standard Python venv is an +acceptable fallback: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -e ".[dev]" +``` -We use pre-commit hooks to ensure code quality and consistency. These hooks automatically format code, run linting, and perform other quality checks before each commit. +## โœ… Canonical verification loop -**Install pre-commit hooks:** +Run this before every push: ```bash -# Automated setup (recommended) -./scripts/pre-commit-setup.sh - -# Or manual setup -pip install pre-commit -pre-commit install -pre-commit install --hook-type pre-push +bash scripts/verify.sh ``` -**What the hooks do:** +This is the single source of truth for branch health. It runs: -- **On every commit:** - - Code formatting with `ruff format` (80-char line length) - - Linting with `ruff check --fix` (auto-fixes issues) - - File quality checks (trailing whitespace, EOF, YAML syntax) - - Safety checks (large files, merge conflicts) +1. `ruff format --check` +2. `ruff check` +3. `basedpyright` +4. `lint-imports` +5. `pytest --cov` -- **On push to remote:** - - Full unit test suite via `scripts/unittest.sh` +CI runs the same script. -**Manual execution:** +## ๐Ÿ—๏ธ Current architecture -```bash -# Run formatting and linting -./scripts/lint.sh +The permanent module roots are: -# Run all pre-commit hooks on all files -pre-commit run --all-files +- `quantmind/flows/` โ€” apex orchestration layer +- `quantmind/configs/` โ€” typed inputs and config +- `quantmind/knowledge/` โ€” typed knowledge shapes and provenance +- `quantmind/preprocess/` โ€” fetch + format + clean +- `quantmind/magic.py` โ€” natural-language resolver +- `quantmind/mind/` โ€” upcoming memory/store work +- `quantmind/utils/` โ€” logger only -# Run specific tests -./scripts/unittest.sh tests/quantmind/sources/ -./scripts/unittest.sh all # Run all tests -``` +Do **not** re-introduce the deleted transitional/runtime packages such as +`quantmind.flow`, `quantmind.config`, `quantmind.llm`, or `quantmind.models`. + +## ๐Ÿงญ Design rules -**Troubleshooting:** +- Prefer **pure functions** over framework classes. +- Use the **OpenAI Agents SDK directly**; do not build a new runtime wrapper. +- Keep **Pydantic models at boundaries** and small internal value types simple. +- Use **absolute imports** across module boundaries. +- Keep **comments and docstrings in English** with Google-style formatting. +- Preserve architectural boundaries enforced by `import-linter`. -- If hooks fail, fix the issues and commit again -- To skip hooks temporarily (not recommended): `git commit --no-verify` -- Update hooks: `pre-commit autoupdate` +## ๐Ÿงช Testing expectations -## ๐Ÿ“ Development Standards +- Add or update tests under `tests/` when behavior changes. +- New feature work should cover both success and failure paths. +- Mock external services and network calls. +- Keep coverage above the configured floor. -### Code Requirements -- **Location**: All new code in `quantmind/` module -- **Style**: Google-style docstrings, 80-char line length -- **Architecture**: Abstract base classes + dependency injection -- **Type Safety**: Pydantic models + comprehensive type hints +For implementation work, add a simple example when it helps demonstrate the new +behavior clearly. -### Testing -- **Unit tests**: Required in `tests/quantmind/` (mirror module structure) -- **Coverage**: Test success and error cases -- **Mocking**: Mock external APIs and file systems +## ๐Ÿงฉ Common contribution paths -### Documentation -- **Examples**: Add to `examples/quantmind/` for new features -- **Docstrings**: Google-style format for all public methods +### New knowledge type -## ๐Ÿ—๏ธ Contribution Types +- Add a schema under `quantmind/knowledge/` +- Choose the right base shape (`FlattenKnowledge`, `TreeKnowledge`, or future + `GraphKnowledge`) +- Implement `embedding_text()` +- Add tests under `tests/knowledge/` -### New Sources -- Extend `BaseSource[ContentType]` in `quantmind/sources/` -- Add config in `quantmind/config/sources.py` -- Include tests and usage example +### New flow -### New Parsers -- Extend `BaseParser` in `quantmind/parsers/` -- Handle multiple content formats with error handling +- Add a typed input/config module under `quantmind/configs/` +- Add a pure `async def ..._flow(...)` under `quantmind/flows/` +- Use `preprocess/` helpers instead of duplicating fetch/format logic +- Return a typed knowledge object +- Add tests under `tests/flows/` -### New Taggers -- Extend `BaseTagger` in `quantmind/tagger/` -- Support rule-based and ML approaches +### New source or format support -### Storage Backends -- Extend `BaseStorage` in `quantmind/storage/` -- Implement indexing, querying, and concurrent access +- Add leaf functionality under `quantmind/preprocess/` +- Keep `preprocess/` independent of `configs/`, `knowledge/`, and `flows/` +- Add focused tests under `tests/preprocess/` -## ๐Ÿ”„ Pull Request Process +### New domain extension -1. **Create feature branch** from `master` -2. **Follow conventional commits**: `type(scope): description` -3. **Pre-commit hooks** run automatically on commit/push -4. **Before submitting**: - ```bash - pre-commit run --all-files - ./scripts/unittest.sh all - ``` -5. **Submit PR** using our template +If you are adapting QuantMind beyond finance, read +`docs/ARCHITECTURE_FOR_NEW_DOMAINS.md` first and keep the extension aligned with +the existing layering. -### PR Checklist -- [ ] Code in `quantmind/` following architecture patterns -- [ ] Unit tests with comprehensive coverage -- [ ] Usage example (for new features) -- [ ] All pre-commit hooks pass -- [ ] Conventional commit format +## ๐Ÿ”„ Pull request expectations -## ๐Ÿ’ก Development Tips +Before submitting a PR: ```bash -# Run specific tests -pytest tests/quantmind/sources/ -pytest tests/quantmind/models/ +pre-commit run --all-files +bash scripts/verify.sh +``` -# Test CLI functionality -quantmind extract "test query" --max-papers 5 -quantmind config show +Also make sure: -# Check code quality -./scripts/lint.sh -``` +- commits use conventional-commit style (`feat:`, `fix:`, `docs:`, `refactor:`) +- PR titles and descriptions are written in English +- docs are updated when behavior or extension points change + +## ๐Ÿค– Agent-facing documentation + +QuantMind is increasingly consumed by AI agents as well as humans. Treat the +following files as product surfaces: + +- `README.md` +- `CONTRIBUTING.md` +- `docs/ARCHITECTURE_FOR_NEW_DOMAINS.md` + +If you change architecture, memory semantics, or extension paths, update the +relevant documentation in the same PR. During active iteration, these agent- +facing docs should be reviewed regularly so agent workflows do not drift away +from the real codebase. -## โ“ Questions? +## โ“Questions? - Check existing [issues](https://github.com/LLMQuant/quant-mind/issues) -- Review architecture patterns in existing code -- Look at `examples/` for usage patterns -- See `CLAUDE.md` for detailed architecture +- Review `CLAUDE.md` for the detailed architecture context +- Read `docs/ARCHITECTURE_FOR_NEW_DOMAINS.md` for extension guidance -Thank you for contributing! ๐Ÿš€ +Thank you for contributing. diff --git a/README.md b/README.md index fb6390f..6d82a38 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,45 @@ 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 enforced at runtime. +This maps directly to loop SLI/SLO operations: every loop is expected to be +traceable, budgeted, and quality-gated before durable writes. --- @@ -107,7 +137,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:** @@ -154,7 +184,7 @@ We use [uv](https://github.com/astral-sh/uv) for fast and reliable Python packag ### ๐Ÿ“š Usage Examples -#### Run a single paper through `paper_flow` +#### Run a single finance paper through `paper_flow` ```python import asyncio @@ -172,6 +202,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()) ``` @@ -224,32 +276,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`):** @@ -265,7 +346,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. ------ @@ -279,11 +361,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 ``` @@ -295,7 +377,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..d0c7304 --- /dev/null +++ b/docs/fincept_integration.md @@ -0,0 +1,64 @@ +# Fincept โ†” QuantMind Integration Guide + +## Overview + +QuantMind serves as the **knowledge extraction layer** for Fincept AI Ops. +Rather than raw market data, Fincept can query QuantMind for: +- Distilled research insights from financial papers +- Structured signal metadata (alpha factors, risk signals) +- Natural language Q&A over quantitative finance literature + +## Integration Architecture + +``` +fincept-ai-ops (FastAPI) + โ”‚ + โ”œโ”€โ”€ GET /quant/search?q={query} + โ”‚ โ†“ + โ”‚ QuantMind Knowledge API + โ”‚ โ†“ + โ”‚ Vector Search (LanceDB / Chroma) + โ”‚ โ†“ + โ”‚ Extracted Insights JSON + โ”‚ + โ””โ”€โ”€ MCP Tool: query_quantmind +``` + +## Usage Example + + 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. Clone both repos side-by-side +2. Set `QUANTMIND_API_URL` in fincept `.env` +3. Run `quantmind serve --port 8001` +4. Fincept auto-discovers at startup + +See [fincept-ai-ops ROADMAP](../fincept-ai-ops/ROADMAP.md) for v1.2 integration milestone. 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..ab496f6 --- /dev/null +++ b/quantmind/flows/governance.py @@ -0,0 +1,210 @@ +"""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 + + +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.""" + + 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 + + +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.""" + rules = policy.l3_commit + confidence = float(artifact.get("validation_confidence", 0.0)) + if confidence < rules.min_confidence: + return False + if rules.require_provenance and not artifact.get("provenance"): + return False + if rules.require_schema_validity and not schema_valid: + return False + if rules.require_dedup_check and not dedup_ok: + return False + if rules.require_contradiction_check and not contradiction_free: + return False + return True + + +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..f0565a1 --- /dev/null +++ b/tests/configs/test_recoverable.py @@ -0,0 +1,39 @@ +"""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") + self.assertIn("Extra inputs are not permitted", 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..f5e5580 --- /dev/null +++ b/tests/flows/test_governance.py @@ -0,0 +1,135 @@ +"""Tests for ``quantmind.flows.governance``.""" + +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: + empty_path = Path(__file__).with_name("empty_governance.yaml") + empty_path.write_text("", encoding="utf-8") + self.addCleanup(empty_path.unlink) + 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](