From 393ff071ba9925b2470ec08731076dce7e9e699f Mon Sep 17 00:00:00 2001 From: pkuwkl Date: Sat, 25 Apr 2026 22:07:52 +0800 Subject: [PATCH 1/3] refactor: remove self-built agent runtime Clears the way for migrating Agent capabilities to OpenAI Agents SDK (see archive/agent-runtime-final on origin for the snapshot of the removed code; PR2-PR4 will rebuild on the new architecture). Modules deleted (no internal dependents remain in surviving code): - quantmind/brain/ (MultiStepAgent / ToolCallingAgent / Memory) - quantmind/tools/ (custom Tool ABC + validators; replaced by SDK @function_tool) - quantmind/storage/ (no users; future KnowledgeStore will live in knowledge/) - quantmind/tagger/ (tagging becomes a flow-level concern) - quantmind/models/{agent,memory,messages}.py - quantmind/utils/{agentic_ext,monitoring}.py Also removed: - vendored smolagents/ working copy - LICENSE-APACHE (no Apache-licensed code remains) - All examples (will be re-added per-flow in later PRs) - Tests for the deleted modules (tests/{brain,tools,storage,tagger}/ and the agent/memory/messages model tests) - Stale dead deps from pyproject.toml: camel-ai, networkx, scikit-learn, pyvis, plotly - Stale CLI entrypoint reference (quantmind_cli.py never existed) Modules retained for now (will be replaced/migrated in PR2-PR4 per the new architecture): - quantmind/flow/, llm/, parsers/, sources/, config/ - quantmind/models/{content,paper,analysis}.py Verification: 192 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 + CLAUDE.md | 193 ++--- LICENSE-APACHE | 201 ----- README.md | 26 +- examples/basic_usage.py | 216 ------ examples/config/config_example.py | 94 --- examples/config/sample_config.yaml | 61 -- examples/config_example.py | 306 -------- examples/flow/01_custom_flow/.env.example | 17 - examples/flow/01_custom_flow/README.md | 71 -- examples/flow/01_custom_flow/config.yaml | 13 - .../flows/greeting_flow/__init__.py | 5 - .../flows/greeting_flow/flow.py | 62 -- .../flows/greeting_flow/prompts.yaml | 15 - examples/flow/01_custom_flow/pipeline.py | 66 -- examples/flow/02_summary_flow/.env.example | 17 - examples/flow/02_summary_flow/README.md | 93 --- examples/flow/02_summary_flow/config.yaml | 9 - .../flows/summary_flow/__init__.py | 5 - .../flows/summary_flow/flow.py | 74 -- .../flows/summary_flow/prompts.yaml | 20 - examples/flow/02_summary_flow/mock_data.py | 127 ---- examples/flow/02_summary_flow/pipeline.py | 85 --- examples/flow/03_podcast_flow/README.md | 110 --- examples/flow/03_podcast_flow/config.yaml | 16 - .../flows/podcast_flow/__init__.py | 9 - .../flows/podcast_flow/flow.py | 32 - .../flows/podcast_flow/prompts.yaml | 8 - examples/flow/03_podcast_flow/pipeline.py | 72 -- examples/flow/README.md | 92 --- examples/llm/embedding_block_example.py | 205 ----- examples/llm/llm_block_example.py | 82 -- examples/memory/basic_memory_usage.py | 66 -- examples/parser/llama_parser_example.ipynb | 115 --- examples/parser/simple_llama_parser.py | 306 -------- examples/parser/test-pdf.pdf | Bin 173133 -> 0 bytes examples/pipeline/arxiv_llama_pipeline.py | 87 --- examples/pipeline/arxiv_storage_pipeline.py | 39 - examples/pipeline/quant_paper/.env.example | 12 - examples/pipeline/quant_paper/README.md | 221 ------ examples/pipeline/quant_paper/config.yaml | 68 -- .../quant_paper/flows/qa_flow/__init__.py | 5 - .../quant_paper/flows/qa_flow/flow.py | 245 ------ .../quant_paper/flows/qa_flow/prompts.yaml | 49 -- .../flows/summary_flow/prompts.yaml | 32 - examples/pipeline/quant_paper/pipeline.py | 314 -------- examples/sources/README.md | 212 ------ examples/sources/advanced_configuration.py | 294 ------- examples/sources/basic_arxiv_usage.py | 220 ------ examples/storage/local_storage_usage.py | 90 --- examples/storage/storage_performance_demo.py | 244 ------ examples/tagger/llm_tagger_example.py | 97 --- examples/tools/basic_tool.py | 36 - examples/tools/pricing_tool.py | 35 - examples/tools/text_stats_tool.py | 32 - examples/utils/logger_demo.py | 221 ------ examples/utils/prompts.yaml | 35 - examples/utils/tmp_usage.py | 46 -- pyproject.toml | 8 - quantmind/brain/__init__.py | 3 - quantmind/brain/memory.py | 154 ---- quantmind/models/memory.py | 253 ------- quantmind/models/messages.py | 447 ----------- quantmind/storage/__init__.py | 6 - quantmind/storage/base.py | 213 ------ quantmind/storage/local_storage.py | 558 -------------- quantmind/tagger/__init__.py | 6 - quantmind/tagger/base.py | 103 --- quantmind/tagger/llm_tagger.py | 267 ------- quantmind/tools/__init__.py | 15 - quantmind/tools/_function_type_hints_utils.py | 472 ------------ quantmind/tools/_tool_validation.py | 300 -------- quantmind/tools/base.py | 715 ------------------ quantmind/tools/utils.py | 231 ------ quantmind/utils/agentic_ext.py | 554 -------------- quantmind/utils/monitoring.py | 302 -------- tests/brain/test_memory.py | 93 --- tests/models/test_memory_models.py | 108 --- tests/models/test_messages_models.py | 104 --- tests/storage/test_local_storage.py | 410 ---------- tests/tagger/test_llm_tagger.py | 391 ---------- tests/tools/test_base_tool.py | 230 ------ tests/tools/test_function_type_hints_utils.py | 553 -------------- tests/tools/test_tool_validation_module.py | 215 ------ tests/tools/test_utils_module.py | 74 -- 85 files changed, 61 insertions(+), 12251 deletions(-) delete mode 100644 LICENSE-APACHE delete mode 100644 examples/basic_usage.py delete mode 100644 examples/config/config_example.py delete mode 100644 examples/config/sample_config.yaml delete mode 100644 examples/config_example.py delete mode 100644 examples/flow/01_custom_flow/.env.example delete mode 100644 examples/flow/01_custom_flow/README.md delete mode 100644 examples/flow/01_custom_flow/config.yaml delete mode 100644 examples/flow/01_custom_flow/flows/greeting_flow/__init__.py delete mode 100644 examples/flow/01_custom_flow/flows/greeting_flow/flow.py delete mode 100644 examples/flow/01_custom_flow/flows/greeting_flow/prompts.yaml delete mode 100644 examples/flow/01_custom_flow/pipeline.py delete mode 100644 examples/flow/02_summary_flow/.env.example delete mode 100644 examples/flow/02_summary_flow/README.md delete mode 100644 examples/flow/02_summary_flow/config.yaml delete mode 100644 examples/flow/02_summary_flow/flows/summary_flow/__init__.py delete mode 100644 examples/flow/02_summary_flow/flows/summary_flow/flow.py delete mode 100644 examples/flow/02_summary_flow/flows/summary_flow/prompts.yaml delete mode 100644 examples/flow/02_summary_flow/mock_data.py delete mode 100644 examples/flow/02_summary_flow/pipeline.py delete mode 100644 examples/flow/03_podcast_flow/README.md delete mode 100644 examples/flow/03_podcast_flow/config.yaml delete mode 100644 examples/flow/03_podcast_flow/flows/podcast_flow/__init__.py delete mode 100644 examples/flow/03_podcast_flow/flows/podcast_flow/flow.py delete mode 100644 examples/flow/03_podcast_flow/flows/podcast_flow/prompts.yaml delete mode 100644 examples/flow/03_podcast_flow/pipeline.py delete mode 100644 examples/flow/README.md delete mode 100644 examples/llm/embedding_block_example.py delete mode 100644 examples/llm/llm_block_example.py delete mode 100644 examples/memory/basic_memory_usage.py delete mode 100644 examples/parser/llama_parser_example.ipynb delete mode 100644 examples/parser/simple_llama_parser.py delete mode 100644 examples/parser/test-pdf.pdf delete mode 100644 examples/pipeline/arxiv_llama_pipeline.py delete mode 100644 examples/pipeline/arxiv_storage_pipeline.py delete mode 100644 examples/pipeline/quant_paper/.env.example delete mode 100644 examples/pipeline/quant_paper/README.md delete mode 100644 examples/pipeline/quant_paper/config.yaml delete mode 100644 examples/pipeline/quant_paper/flows/qa_flow/__init__.py delete mode 100644 examples/pipeline/quant_paper/flows/qa_flow/flow.py delete mode 100644 examples/pipeline/quant_paper/flows/qa_flow/prompts.yaml delete mode 100644 examples/pipeline/quant_paper/flows/summary_flow/prompts.yaml delete mode 100644 examples/pipeline/quant_paper/pipeline.py delete mode 100644 examples/sources/README.md delete mode 100644 examples/sources/advanced_configuration.py delete mode 100644 examples/sources/basic_arxiv_usage.py delete mode 100644 examples/storage/local_storage_usage.py delete mode 100644 examples/storage/storage_performance_demo.py delete mode 100644 examples/tagger/llm_tagger_example.py delete mode 100644 examples/tools/basic_tool.py delete mode 100644 examples/tools/pricing_tool.py delete mode 100644 examples/tools/text_stats_tool.py delete mode 100644 examples/utils/logger_demo.py delete mode 100644 examples/utils/prompts.yaml delete mode 100644 examples/utils/tmp_usage.py delete mode 100644 quantmind/brain/__init__.py delete mode 100644 quantmind/brain/memory.py delete mode 100644 quantmind/models/memory.py delete mode 100644 quantmind/models/messages.py delete mode 100644 quantmind/storage/__init__.py delete mode 100644 quantmind/storage/base.py delete mode 100644 quantmind/storage/local_storage.py delete mode 100644 quantmind/tagger/__init__.py delete mode 100644 quantmind/tagger/base.py delete mode 100644 quantmind/tagger/llm_tagger.py delete mode 100644 quantmind/tools/__init__.py delete mode 100644 quantmind/tools/_function_type_hints_utils.py delete mode 100644 quantmind/tools/_tool_validation.py delete mode 100644 quantmind/tools/base.py delete mode 100644 quantmind/tools/utils.py delete mode 100644 quantmind/utils/agentic_ext.py delete mode 100644 quantmind/utils/monitoring.py delete mode 100644 tests/brain/test_memory.py delete mode 100644 tests/models/test_memory_models.py delete mode 100644 tests/models/test_messages_models.py delete mode 100644 tests/storage/test_local_storage.py delete mode 100644 tests/tagger/test_llm_tagger.py delete mode 100644 tests/tools/test_base_tool.py delete mode 100644 tests/tools/test_function_type_hints_utils.py delete mode 100644 tests/tools/test_tool_validation_module.py delete mode 100644 tests/tools/test_utils_module.py diff --git a/.gitignore b/.gitignore index 2ce114b..6a9f5a2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,7 @@ papers/ data/ demo_data/ *.html + +# Local-only development scratch notes (not tracked) +new_feature.md +docs/design/zh/next-step-architecture.md diff --git a/CLAUDE.md b/CLAUDE.md index 482a249..c90ed3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,14 +4,21 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -QuantMind is an intelligent knowledge extraction and retrieval framework for quantitative finance. It transforms unstructured financial content into a queryable knowledge graph using a two-stage architecture: - -**Stage 1: Knowledge Extraction** (Current Implementation) -- Source APIs → Intelligent Parser → Workflow/Agent → Structured Knowledge Base -- Components: Crawlers, Parsers, Taggers, Workflow orchestration, Storage - -**Stage 2: Intelligent Retrieval** (Future) -- Knowledge Base → Embeddings → Solution Scenarios (DeepResearch, RAG, Data MCP) +QuantMind is an intelligent knowledge extraction and retrieval framework for quantitative +finance. It is being repositioned as a domain library that runs on top of OpenAI Agents +SDK rather than as a self-contained agent framework. The next-step architecture +introduces these top-level modules: + +- `flows/` — e2e processing pipelines (Agent runtime delegated to OpenAI Agents SDK) +- `knowledge/` — Pydantic-based knowledge schema standard +- `preprocess/` — fetching and formatting helpers (PDF/HTML → markdown, etc.) +- `mind/` — QuantMind's distinctive cognitive layer (working memory MVP first) +- `configs/` — centralized flow/input config types +- `magic.py` — natural-language → (input, cfg) resolver + +Until those modules land, the repository is in a transitional state. PR1 removes the +self-built agent runtime so subsequent PRs can build the new architecture from a clean +slate. ## Development Commands @@ -42,98 +49,33 @@ pytest tests/quantmind/ # new quantmind tests pytest tests/quantmind/models/ # specific module ``` -### QuantMind CLI Usage -```bash -# Basic extraction -quantmind extract "machine learning finance" --max-papers 10 - -# Full pipeline with storage -quantmind pipeline ml_pipeline "cat:q-fin.ST" --storage json --tagger rule - -# Search stored papers -quantmind search --categories "Machine Learning in Finance" --limit 5 - -# System status -quantmind status - -# Configuration management -quantmind config create --output config.yaml -quantmind config show -``` - -### Legacy System -```bash -# Old autoscholar system (still available) -python quant_scholar.py -``` - -## New Architecture (QuantMind v0.2.0) - -### Core Modules - -**quantmind/** - New modular architecture following Stage 1 design: +## Current Modules (transitional, PR1) -- **sources/**: Content acquisition layer - - `base.py`: Abstract source interface - - `arxiv_source.py`: ArXiv API integration with financial focus +After PR1's removal, the surviving modules are: -- **parsers/**: Content processing layer - - `base.py`: Abstract parser interface - - `pdf_parser.py`: PDF extraction (PyMuPDF + Marker support) +- **flow/**: Existing flow scaffolding (will be replaced by `flows/` in a later PR) +- **parsers/**: PDF / Llama parser helpers (will move to `preprocess/format/`) +- **sources/**: ArXiv source (fetch logic will move to `preprocess/fetch/`) +- **config/**: Configuration management (will be replaced by `configs/`) +- **llm/**: LLM block + embedding helpers (will be removed once `flow/` migrates) +- **models/**: `Paper`, `BaseContent`, `KnowledgeItem`, `analysis` (will move to `knowledge/`) +- **utils/**: `logger.py` (kept long-term) plus tmp helpers -- **tagger/**: Classification and labeling layer - - `base.py`: Abstract tagger interface - - `rule_tagger.py`: Rule-based financial classification - - `llm_tagger.py`: LLM-powered advanced tagging - -- **workflow/**: Orchestration layer - - `agent.py`: Main WorkflowAgent for pipeline coordination - - `pipeline.py`: Pipeline execution with dependency management - - `tasks.py`: Task definitions (Crawl, Parse, Tag, Store) - -- **storage/**: Knowledge base layer - - `base.py`: Abstract storage interface - - `json_storage.py`: JSON file-based storage with indexing - -- **models/**: Data models - - `paper.py`: Enhanced Paper model with Pydantic validation - - `knowledge_graph.py`: Advanced graph operations - -- **config/**: Configuration management - - `settings.py`: Structured configuration with validation - -- **utils/**: Shared utilities - - `logger.py`: Consistent logging setup - -### Examples and Usage - -- **examples/quantmind/**: Complete usage examples - - `basic_usage.py`: Basic pipeline demonstration - - `config_example.py`: Configuration system demo - -### Legacy System (autoscholar/) - -Still available for backward compatibility: -- **crawler/**: Legacy crawlers -- **parser/**: Legacy parsers -- **knowledge/**: Legacy graph models -- **visualization/**: Pyvis visualizations +These modules continue to compile and ship as-is in PR1; their replacements arrive in +PR2 (`knowledge/` + `configs/`), PR3 (`preprocess/`), and PR4 (`flows/` + drop `flow/` `llm/`). ## Key Dependencies ### Core Dependencies - Pydantic for data validation -- NetworkX for graph operations -- PyMuPDF for PDF processing +- PyMuPDF / Marker for PDF processing - ArXiv API client -- YAML for configuration -- Requests for HTTP operations +- LiteLLM for multi-provider LLM access +- YAML / Requests / httpx for configuration and IO ### Optional Dependencies -- OpenAI API (for LLM tagger) -- CAMEL-AI (alternative LLM framework) -- Marker (AI-powered PDF parsing) -- PyVis (graph visualization) +- OpenAI API +- llama-cloud-services (Llama parser) ## Development Guidelines @@ -163,64 +105,23 @@ Still available for backward compatibility: - **Quality Control**: Built-in deduplication and validation - **Extensibility**: Easy to add new sources, parsers, taggers, storage -## Migration Notes +## User Development Guidance + +- Config should add in `quantmind/config` (until `configs/` lands in a later PR) +- Data models should add in `quantmind/models` (until `knowledge/` lands) +- Do not use `Dict[str, Any]` in initialize functions — not type safe. +- Do not overdesign — implement the basic and straightforward code, refactor later. +- Tests in `tests//`, inherit `unittest.TestCase`. -When migrating from autoscholar to quantmind: -1. Use `WorkflowAgent` instead of direct crawler usage -2. Configure components via `Settings` system -3. Use the CLI for common operations -4. Take advantage of new pipeline orchestration -5. Leverage improved error handling and logging +## PR1 Cleanup (2026-04-25) -## User Development Guidance +PR1 removes the self-built agent runtime to make room for the OpenAI Agents SDK +migration. Removed in PR1: -- Config should add in `quantmind/config` -- Data models should add in `quantmind/models` -- Initialize function can not use `Dict[str, Any]`, which is not type safe. -- Do not overdesign the code, just implement the basic and straightforward code, since we can always refactor the code later. -- For examples, add in `quantmind/examples`, and just demo the simple usage. (do not add too many use cases in single file) -- For tests, add in `tests/`, and inherit the `unittest.TestCase` class. - -## Tagger Module Refactoring (v0.0.1) - -### Simplified LLM Tagger Design -The tagger module has been completely refactored to eliminate over-engineering: - -**Removed Components:** -- `rule_tagger.py` - Removed rule-based tagger (obsolete in LLM era) -- `PaperTag` class - Removed complex tag objects, use simple strings -- `hierarchical_tags` feature - Removed unnecessary complexity -- `confidence_score` calculations - LLM outputs are inherently probabilistic -- Categories vs Tags distinction - Unified to use only tags - -**Simplified LLMTagger:** -- **Type-safe configuration**: Uses `LLMTaggerConfig` Pydantic model instead of `Dict[str, Any]` -- **Structured imports**: `from quantmind.config import LLMTaggerConfig` and `from quantmind.models import Paper` -- **Clean interface**: Single `config` parameter with proper type hints -- **Base tagger**: Simplified to only require `tag_paper()` and `extract_tags()` abstract methods -- **Custom instructions**: Support for user-provided instructions via `config.custom_instructions` -- **Flexible LLM support**: `config.llm_type` and `config.llm_name` for different providers -- **Base URL support**: `config.base_url` for custom API endpoints - -**Configuration Structure:** -```python -from quantmind.config import LLMTaggerConfig - -config = LLMTaggerConfig( - llm_type="openai", - llm_name="gpt-4o", - max_tags=5, - custom_instructions="Focus on trading strategies", - api_key="your-api-key", - base_url="https://custom-endpoint.com" # optional -) - -tagger = LLMTagger(config=config) -``` +- `quantmind/brain/`, `quantmind/tools/`, `quantmind/storage/`, `quantmind/tagger/` +- `quantmind/models/{agent,memory,messages}.py` +- `quantmind/utils/{agentic_ext,monitoring}.py` +- vendored `smolagents/` and `LICENSE-APACHE` +- All examples (will be re-added per-flow in later PRs) -**Design Principles:** -- **No over-engineering**: Simple, direct implementation -- **Type safety**: Proper Pydantic configuration models -- **User-friendly**: Clear API with sensible defaults -- **Extensible**: Easy to add new LLM providers through config -- **Maintainable**: ~290 lines vs previous 800+ lines +Preserved as historical reference: `archive/agent-runtime-final` branch. diff --git a/LICENSE-APACHE b/LICENSE-APACHE deleted file mode 100644 index 8893ab4..0000000 --- a/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - -Copyright {yyyy} {name of copyright owner} - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/README.md b/README.md index 186dd2f..bdc9097 100644 --- a/README.md +++ b/README.md @@ -192,10 +192,10 @@ You can take [this example](examples/basic_usage.py) as a reference. - [x] Better `flow` design for user-friendly usage - [x] First production level example (Quant Paper Agent) -- [x] `tool` integration for external information & agentic capabilities -- [ ] Agentic Orchestration (`LLMFlow`) for more advanced usage +- [ ] Migrate Agent layer to OpenAI Agents SDK +- [ ] Standardize knowledge format with `knowledge/` (Pydantic-based) - [ ] Additional content sources (financial news, blogs, reports) -- [ ] Standardize the `knowledge` format (data standardization) +- [ ] Cross-step working memory (`mind/memory`) for batch document processing --- @@ -213,16 +213,13 @@ The foundation we're building today—starting with papers—will expand to enco > > ```python > # The future we are building towards -> from quantmind import KnowledgeBase, MemoryBank -> from quantmind.agents import PaperReader, NewsMonitor -> from quantmind.brain import understand, memorize, recall +> from quantmind.flows import paper_flow, batch_run +> from quantmind.knowledge import Paper +> from quantmind.mind.memory import FilesystemMemory > -> # Initialize the knowledge base -> kb = KnowledgeBase() -> kb.ingest(source="arxiv", topic="portfolio optimization") -> -> # Query for high-level insights -> insights = kb.query("latest trends in risk parity strategies") +> memory = FilesystemMemory("./mem/factor-research/") +> for arxiv_id in arxiv_ids: +> 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. @@ -259,12 +256,9 @@ We welcome contributions of all forms, from bug reports to feature development. ### License -QuantMind is released under the MIT License—see `LICENSE` for details. Portions of the -agent tooling system are derived from Hugging Face's `smolagents` project and are -provided under the Apache License 2.0 in `LICENSE-APACHE`. +QuantMind is released under the MIT License—see `LICENSE` for details. ### ❤️ Acknowledgements - **arXiv** for providing open access to a world of research. - The **open-source community** for the tools and libraries that make this project possible. -- Hugging Face for `smolagents`, which inspired and informed our agent tooling / runtime abstractions. diff --git a/examples/basic_usage.py b/examples/basic_usage.py deleted file mode 100644 index 7ddd8fe..0000000 --- a/examples/basic_usage.py +++ /dev/null @@ -1,216 +0,0 @@ -#!/usr/bin/env python3 -"""Basic usage example for QuantMind Stage 1 architecture. - -This example demonstrates how to use the new QuantMind architecture to: -1. Set up sources, parsers, taggers, and storage -2. Create and execute a knowledge extraction pipeline -3. Process financial research papers from arXiv -""" - -import os -import sys -from pathlib import Path - -# Add the parent directory to the path so we can import quantmind -sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from quantmind.workflow.agent import WorkflowAgent -from quantmind.sources.arxiv_source import ArxivSource -from quantmind.parsers.pdf_parser import PDFParser -from quantmind.tagger.rule_tagger import RuleTagger -from quantmind.tagger.llm_tagger import LLMTagger -from quantmind.storage.json_storage import JSONStorage -from quantmind.config.settings import create_default_config -from quantmind.utils.logger import setup_logger, get_logger - -# Set up logging -setup_logger(level=20) # INFO level -logger = get_logger(__name__) - - -def main(): - """Run the basic QuantMind usage example.""" - logger.info("Starting QuantMind basic usage example") - - # 1. Create workflow agent - agent = WorkflowAgent( - config={ - "max_workers": 2, - "retry_attempts": 2, - "timeout": 180, - "enable_deduplication": True, - } - ) - - # 2. Register components - logger.info("Registering components...") - - # Register ArXiv source - arxiv_source = ArxivSource( - config={"max_results": 50, "sort_by": "SubmittedDate"} - ) - agent.register_source("arxiv", arxiv_source) - - # Register PDF parser (optional, for full text extraction) - pdf_parser = PDFParser( - config={ - "method": "pymupdf", # Use PyMuPDF for simplicity - "download_pdfs": False, # Skip PDF download for this example - "max_file_size": 10, # MB - } - ) - agent.register_parser("pdf", pdf_parser) - - # Register rule-based tagger - rule_tagger = RuleTagger(config={"case_sensitive": False}) - agent.register_tagger("rule", rule_tagger) - - # Register LLM tagger (optional, requires OpenAI API key) - if os.getenv("OPENAI_API_KEY"): - llm_tagger = LLMTagger( - config={ - "model_type": "openai", - "model_name": "gpt-4", - "temperature": 0.0, - } - ) - agent.register_tagger("llm", llm_tagger) - logger.info("LLM tagger registered (OpenAI API key found)") - else: - logger.info("LLM tagger skipped (no OpenAI API key)") - - # Register JSON storage - json_storage = JSONStorage( - config={ - "storage_dir": "./data/quantmind_example", - "auto_backup": True, - "max_backup_count": 3, - } - ) - agent.register_storage("json", json_storage) - - # 3. Run quick extraction example - logger.info( - "Running quick extraction for machine learning in finance papers..." - ) - - try: - papers = agent.run_quick_extraction( - source_name="arxiv", - query="cat:q-fin.ST OR cat:q-fin.TR OR (machine learning AND finance)", - max_papers=10, - tagger_name="rule", - ) - - logger.info(f"Successfully extracted {len(papers)} papers") - - # 4. Display results - print("\n" + "=" * 80) - print("EXTRACTION RESULTS") - print("=" * 80) - - for i, paper in enumerate(papers, 1): - print(f"\n{i}. {paper.title}") - print( - f" Authors: {', '.join(paper.authors[:3])}{'...' if len(paper.authors) > 3 else ''}" - ) - print(f" Categories: {', '.join(paper.categories)}") - print( - f" Tags: {', '.join(paper.tags[:5])}{'...' if len(paper.tags) > 5 else ''}" - ) - print(f" ArXiv ID: {paper.arxiv_id or 'N/A'}") - print( - f" Published: {paper.published_date.strftime('%Y-%m-%d') if paper.published_date else 'N/A'}" - ) - print(f" Abstract: {paper.abstract[:200]}...") - - except Exception as e: - logger.error(f"Quick extraction failed: {e}") - return - - # 5. Create and execute a full pipeline - logger.info("\nCreating full extraction pipeline...") - - try: - pipeline = agent.create_extraction_pipeline( - name="finance_ml_pipeline", - source_name="arxiv", - query="cat:q-fin.ST AND machine learning", - max_papers=5, - tagger_name="rule", - storage_name="json", - ) - - logger.info("Executing pipeline...") - results = agent.execute_pipeline("finance_ml_pipeline") - - print("\n" + "=" * 80) - print("PIPELINE RESULTS") - print("=" * 80) - - for task_id, result in results.items(): - print(f"\nTask {task_id}: {type(result).__name__}") - if hasattr(result, "__len__"): - print(f" Results count: {len(result)}") - - # Get pipeline statistics - stats = agent.get_pipeline_status("finance_ml_pipeline") - if stats: - print(f"\nPipeline Statistics:") - print(f" Status: {stats['status']}") - print(f" Total tasks: {stats['total_tasks']}") - print(f" Duration: {stats.get('duration', 'N/A')} seconds") - print(f" Task counts: {stats['task_counts']}") - - except Exception as e: - logger.error(f"Pipeline execution failed: {e}") - return - - # 6. Storage examples - logger.info("\nTesting storage operations...") - - try: - # Get storage statistics - storage_info = json_storage.get_storage_info() - print(f"\nStorage Info:") - print(f" Type: {storage_info['type']}") - print(f" Paper count: {storage_info['paper_count']}") - - # Search examples - if storage_info["paper_count"] > 0: - # Search by category - ml_papers = json_storage.search_papers( - categories=["Machine Learning in Finance"], limit=5 - ) - print(f" ML papers found: {len(ml_papers)}") - - # Get all categories - categories = json_storage.get_categories() - print( - f" Categories: {', '.join(categories[:5])}{'...' if len(categories) > 5 else ''}" - ) - - # Get all tags - tags = json_storage.get_tags() - print( - f" Tags: {', '.join(tags[:10])}{'...' if len(tags) > 10 else ''}" - ) - - except Exception as e: - logger.error(f"Storage operations failed: {e}") - - # 7. Show execution history - history = agent.get_execution_history() - if history: - print(f"\nExecution History: {len(history)} pipeline runs") - for execution in history[-3:]: # Show last 3 - print(f" {execution['pipeline_name']}: {execution['status']}") - - print("\n" + "=" * 80) - print("Example completed successfully!") - print("Check ./data/quantmind_example/ for stored papers") - print("=" * 80) - - -if __name__ == "__main__": - main() diff --git a/examples/config/config_example.py b/examples/config/config_example.py deleted file mode 100644 index e07625f..0000000 --- a/examples/config/config_example.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Example: Using the new unified configuration system. - -This example demonstrates how to: -1. Load configuration from YAML with environment variable substitution -2. Create default configuration -3. Save configuration to YAML -4. Access configuration values -""" - -import os -from pathlib import Path - -from quantmind.config import Setting, create_default_config, load_config - - -def main(): - """Demonstrate configuration usage.""" - print("QuantMind Configuration System Example") - print("=" * 40) - - # Example 1: Create default configuration - print("\n1. Creating default configuration:") - default_setting = create_default_config() - print(f" Source type: {type(default_setting.source).__name__}") - print(f" Parser type: {type(default_setting.parser).__name__}") - print(f" Storage directory: {default_setting.storage.storage_dir}") - - # Example 2: Save default configuration to YAML - print("\n2. Saving default configuration to YAML:") - config_dir = Path("examples/config") - config_dir.mkdir(exist_ok=True) - default_config_path = config_dir / "default_config.yaml" - default_setting.save_to_yaml(default_config_path) - print(f" Saved to: {default_config_path}") - - # Example 3: Load configuration from YAML - print("\n3. Loading configuration from YAML:") - sample_config_path = config_dir / "sample_config.yaml" - - if sample_config_path.exists(): - try: - # Load configuration with environment variable substitution - setting = load_config(sample_config_path) - print(f" ✅ Loaded configuration from {sample_config_path}") - print(f" Source: {setting.source}") - print(f" Parser: {setting.parser}") - print(f" Log level: {setting.log_level}") - - # Access specific configuration values - if setting.source: - print(f" Source max_results: {setting.source.max_results}") - - if setting.parser: - print(f" Parser method: {setting.parser.method}") - - except Exception as e: - print(f" ❌ Failed to load configuration: {e}") - else: - print(f" ⚠️ Sample config not found at {sample_config_path}") - - # Example 4: Environment variable substitution - print("\n4. Environment variable substitution:") - print(" Set these environment variables to see substitution in action:") - print(" - ARXIV_MAX_RESULTS=50") - print(" - OPENAI_MODEL=gpt-3.5-turbo") - print(" - LOG_LEVEL=DEBUG") - - env_vars = ["ARXIV_MAX_RESULTS", "OPENAI_MODEL", "LOG_LEVEL"] - for var in env_vars: - value = os.getenv(var) - status = "✅ Set" if value else "❌ Not set" - print(f" {var}: {status}" + (f" = {value}" if value else "")) - - # Example 5: Direct configuration creation - print("\n5. Creating configuration programmatically:") - from quantmind.config import ArxivSourceConfig, LLMConfig, PDFParserConfig - - custom_setting = Setting( - source=ArxivSourceConfig( - max_results=50, sort_by="relevance", download_pdfs=True - ), - parser=PDFParserConfig(method="pymupdf", extract_tables=True), - llm=LLMConfig(model="gpt-4o", temperature=0.3), - log_level="DEBUG", - ) - - print(f" ✅ Created custom configuration") - print(f" Source max_results: {custom_setting.source.max_results}") - print(f" Parser method: {custom_setting.parser.method}") - print(f" LLM model: {custom_setting.llm.model}") - - -if __name__ == "__main__": - main() diff --git a/examples/config/sample_config.yaml b/examples/config/sample_config.yaml deleted file mode 100644 index 2038e36..0000000 --- a/examples/config/sample_config.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# QuantMind Configuration Example -# This file demonstrates the new unified configuration system - -# Source configuration (single instance) -source: - type: arxiv - config: - max_results: ${ARXIV_MAX_RESULTS:100} - sort_by: submittedDate - sort_order: descending - download_pdfs: true - requests_per_second: 1.0 - -# Parser configuration (single instance) -parser: - type: pdf - config: - method: pymupdf - download_pdfs: true - extract_tables: true - extract_images: false - max_file_size_mb: 50 - -# Tagger configuration (single instance) -tagger: - type: llm - config: - llm_config: - model: ${OPENAI_MODEL:gpt-4o} - api_key: ${OPENAI_API_KEY} - temperature: 0.3 - max_tokens: 5000 - max_tags: 5 - -# Storage configuration -storage: - type: local - config: - base_dir: ${DATA_DIR:./data} - -# Flow configuration (single instance) -flow: - type: qa - config: - num_questions: 5 - include_different_difficulties: true - llm_config: - model: ${OPENAI_MODEL:gpt-4o} - api_key: ${OPENAI_API_KEY} - temperature: 0.3 - max_tokens: 4000 - -# Core LLM configuration -llm: - model: ${OPENAI_MODEL:gpt-4o} - api_key: ${OPENAI_API_KEY} - temperature: 0.3 - max_tokens: 4000 - -# Global settings -log_level: ${LOG_LEVEL:INFO} diff --git a/examples/config_example.py b/examples/config_example.py deleted file mode 100644 index 34ea86b..0000000 --- a/examples/config_example.py +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/env python3 -"""Configuration example for QuantMind. - -This example demonstrates how to use the configuration system to set up -QuantMind components and workflows. -""" - -import os -import sys -from pathlib import Path - -# Add the parent directory to the path so we can import quantmind -sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - -from quantmind.config.settings import ( - Settings, - create_default_config, - save_config, - load_config, -) -from quantmind.workflow.agent import WorkflowAgent -from quantmind.sources.arxiv_source import ArxivSource -from quantmind.parsers.pdf_parser import PDFParser -from quantmind.tagger.rule_tagger import RuleTagger -from quantmind.tagger.llm_tagger import LLMTagger -from quantmind.storage.json_storage import JSONStorage -from quantmind.utils.logger import setup_logger, get_logger - -# Set up logging -setup_logger() -logger = get_logger(__name__) - - -def create_sample_config(): - """Create a sample configuration for QuantMind.""" - # Start with default configuration - settings = create_default_config() - - # Customize workflow settings - settings.workflow.max_workers = 6 - settings.workflow.retry_attempts = 3 - settings.workflow.timeout = 600 # 10 minutes - settings.workflow.enable_deduplication = True - settings.workflow.quality_threshold = 0.6 - - # Configure sources - settings.sources["arxiv"].config.update( - {"max_results": 200, "rate_limit_delay": 1.0} - ) - - # Add additional source for news - from quantmind.config.settings import SourceConfig - - settings.sources["financial_news"] = SourceConfig( - name="financial_news", - type="NewsSource", - config={ - "api_key": "${NEWS_API_KEY}", - "sources": ["reuters", "bloomberg", "financial-times"], - "max_articles": 100, - }, - enabled=False, # Disabled by default - ) - - # Configure parsers - settings.parsers["pdf"].config.update( - { - "method": "marker", # Use AI-powered parsing - "download_pdfs": True, - "max_file_size": 100, - "cache_dir": "./cache/pdfs", - } - ) - - # Add web parser - from quantmind.config.settings import ParserConfig - - settings.parsers["web"] = ParserConfig( - name="web", - type="WebParser", - config={ - "user_agent": "QuantMind/1.0", - "timeout": 30, - "max_content_length": 1000000, - }, - ) - - # Configure taggers - settings.taggers["rule"].config.update( - { - "case_sensitive": False, - "custom_categories": { - "ESG Finance": ["esg", "sustainable finance", "green bonds"], - "Crypto Finance": [ - "cryptocurrency", - "bitcoin", - "blockchain", - "defi", - ], - "High Frequency Trading": [ - "hft", - "high frequency", - "microsecond", - "latency", - ], - }, - } - ) - - # Enable LLM tagger with custom configuration - settings.taggers["llm"].enabled = True - settings.taggers["llm"].config.update( - { - "model_type": "openai", - "model_name": "gpt-4", - "temperature": 0.1, - "max_tokens": 1500, - "custom_prompt_template": "financial_classification", - } - ) - - # Configure storage - settings.storages["json"].config.update( - { - "storage_dir": "./data/quantmind", - "auto_backup": True, - "max_backup_count": 10, - "compression": True, - } - ) - - # Add database storage option - from quantmind.config.settings import StorageConfig - - settings.storages["database"] = StorageConfig( - name="database", - type="DatabaseStorage", - config={ - "connection_string": "${DATABASE_URL}", - "table_prefix": "quantmind_", - "enable_full_text_search": True, - "connection_pool_size": 5, - }, - enabled=False, # Disabled by default - ) - - # Set global settings - settings.log_level = "INFO" - settings.arxiv_max_results = 500 - - return settings - - -def demonstrate_config_usage(): - """Demonstrate how to use configuration in practice.""" - logger.info("Creating sample configuration...") - - # Create configuration - settings = create_sample_config() - - # Save configuration to file - config_path = Path("./examples/quantmind/sample_config.yaml") - save_config(settings, config_path) - logger.info(f"Saved configuration to {config_path}") - - # Load configuration from file - logger.info("Loading configuration from file...") - loaded_settings = load_config(config_path) - - # Create WorkflowAgent from configuration - agent = WorkflowAgent(config=loaded_settings.workflow.__dict__) - - # Register components based on configuration - logger.info("Registering components from configuration...") - - # Register enabled sources - for name, source_config in loaded_settings.get_enabled_sources().items(): - if source_config.type == "ArxivSource": - source = ArxivSource(config=source_config.config) - agent.register_source(name, source) - logger.info(f"Registered source: {name}") - - # Register enabled parsers - for name, parser_config in loaded_settings.get_enabled_parsers().items(): - if parser_config.type == "PDFParser": - parser = PDFParser(config=parser_config.config) - agent.register_parser(name, parser) - logger.info(f"Registered parser: {name}") - - # Register enabled taggers - for name, tagger_config in loaded_settings.get_enabled_taggers().items(): - if tagger_config.type == "RuleTagger": - tagger = RuleTagger(config=tagger_config.config) - agent.register_tagger(name, tagger) - logger.info(f"Registered tagger: {name}") - elif tagger_config.type == "LLMTagger" and os.getenv("OPENAI_API_KEY"): - tagger = LLMTagger(config=tagger_config.config) - agent.register_tagger(name, tagger) - logger.info(f"Registered tagger: {name}") - - # Register enabled storages - for name, storage_config in loaded_settings.get_enabled_storages().items(): - if storage_config.type == "JSONStorage": - storage = JSONStorage(config=storage_config.config) - agent.register_storage(name, storage) - logger.info(f"Registered storage: {name}") - - # Display agent status - print("\n" + "=" * 60) - print("AGENT CONFIGURATION") - print("=" * 60) - print(f"Sources: {list(agent.sources.keys())}") - print(f"Parsers: {list(agent.parsers.keys())}") - print(f"Taggers: {list(agent.taggers.keys())}") - print(f"Storages: {list(agent.storages.keys())}") - print(f"Max workers: {agent.max_workers}") - print(f"Retry attempts: {agent.retry_attempts}") - print(f"Timeout: {agent.timeout}") - - return agent, loaded_settings - - -def show_configuration_details(settings): - """Show detailed configuration information.""" - print("\n" + "=" * 60) - print("CONFIGURATION DETAILS") - print("=" * 60) - - print(f"\nWorkflow Settings:") - print(f" Max workers: {settings.workflow.max_workers}") - print(f" Retry attempts: {settings.workflow.retry_attempts}") - print(f" Timeout: {settings.workflow.timeout}") - print(f" Deduplication: {settings.workflow.enable_deduplication}") - print(f" Quality threshold: {settings.workflow.quality_threshold}") - - print(f"\nGlobal Settings:") - print(f" Log level: {settings.log_level}") - print(f" Storage directory: {settings.storage.storage_dir}") - print(f" ArXiv max results: {settings.arxiv_max_results}") - - print(f"\nSources ({len(settings.sources)}):") - for name, source in settings.sources.items(): - status = "✓" if source.enabled else "✗" - print(f" {status} {name} ({source.type})") - - print(f"\nParsers ({len(settings.parsers)}):") - for name, parser in settings.parsers.items(): - status = "✓" if parser.enabled else "✗" - print(f" {status} {name} ({parser.type})") - - print(f"\nTaggers ({len(settings.taggers)}):") - for name, tagger in settings.taggers.items(): - status = "✓" if tagger.enabled else "✗" - print(f" {status} {name} ({tagger.type})") - - print(f"\nStorages ({len(settings.storages)}):") - for name, storage in settings.storages.items(): - status = "✓" if storage.enabled else "✗" - print(f" {status} {name} ({storage.type})") - - -def main(): - """Run the configuration example.""" - logger.info("Starting QuantMind configuration example") - - try: - # Create and demonstrate configuration - agent, settings = demonstrate_config_usage() - - # Show configuration details - show_configuration_details(settings) - - # Test a simple extraction if we have components - if agent.sources and agent.taggers: - logger.info("\nTesting configured pipeline...") - - source_name = list(agent.sources.keys())[0] - tagger_name = list(agent.taggers.keys())[0] - - try: - papers = agent.run_quick_extraction( - source_name=source_name, - query="machine learning finance", - max_papers=3, - tagger_name=tagger_name, - ) - - print( - f"\nTest extraction successful: {len(papers)} papers processed" - ) - - except Exception as e: - logger.warning(f"Test extraction failed: {e}") - - print("\n" + "=" * 60) - print("Configuration example completed successfully!") - print("Check sample_config.yaml for the generated configuration") - print("=" * 60) - - except Exception as e: - logger.error(f"Configuration example failed: {e}") - raise - - -if __name__ == "__main__": - main() diff --git a/examples/flow/01_custom_flow/.env.example b/examples/flow/01_custom_flow/.env.example deleted file mode 100644 index 52dba40..0000000 --- a/examples/flow/01_custom_flow/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -# Example environment file for QuantMind flows -# Copy this file to .env and fill in your actual API keys - -# OpenAI API Key (for GPT models) -OPENAI_API_KEY=sk-your-openai-api-key-here - -# Anthropic API Key (for Claude models) -ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key-here - -# Google API Key (for Gemini models) -GOOGLE_API_KEY=your-google-api-key-here - -# DeepSeek API Key (for DeepSeek models) -DEEPSEEK_API_KEY=your-deepseek-api-key-here - -# Custom environment variables (you can define your own) -MY_CUSTOM_LLM_KEY=your-custom-key-here diff --git a/examples/flow/01_custom_flow/README.md b/examples/flow/01_custom_flow/README.md deleted file mode 100644 index 1e2c0a7..0000000 --- a/examples/flow/01_custom_flow/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# Custom Flow Example - -This example demonstrates how to create a simple custom flow using the new QuantMind flow architecture. - -## Structure - -```bash -examples/flow/01_custom_flow/ -├── flows/ -│ └── greeting_flow/ -│ ├── __init__.py # Module exports -│ ├── prompts.yaml # Prompt templates -│ └── flow.py # Flow implementation -├── config.yaml # Flow configuration -├── pipeline.py # Entry point -├── .env.example # Example .env file -└── README.md # This file -``` - -## Key Components - -### 1. Flow Configuration (`GreetingFlowConfig`) - -- Extends `BaseFlowConfig` -- Defines resource requirements (LLM blocks) -- Uses Pydantic models for simplicity -- Use `register_flow_config` decorator to register the flow config - -### 2. Flow Implementation (`GreetingFlow`) - -- Extends `BaseFlow` -- Implements custom `run()` method -- Direct access to LLM blocks: `self._llm_blocks["greeter"]` -- Template rendering: `self._render_prompt("template_name", **vars)` - -### 3. Prompt Templates (`prompts.yaml`) - -- Jinja2 templates with `{{ variable }}` syntax -- Separated from code for easy editing -- Loaded dynamically - -## Easy Import Structure - -With the new `__init__.py` files, you can now import flows cleanly: - -```python -# Import from flow directory -from flows.greeting_flow import GreetingFlow, GreetingFlowConfig - -# Use in major pipelines -flow = GreetingFlow(config) -``` - -## Running the Demo - -```bash -cd examples/flow/01_custom_flow -# Prepare the api-key .env file -cp .env.example .env -python pipeline.py -``` - -## Benefits Demonstrated - -1. **Simple Configuration**: No complex schemas, just resources -2. **Code-based Logic**: Python orchestration instead of config-driven -3. **Direct Access**: No unnecessary wrapper methods -4. **Template Separation**: YAML-based prompts -5. **Type Safety**: Pydantic models configuration - -This showcases the new architecture's core principle: **provide resources, implement logic in code**. diff --git a/examples/flow/01_custom_flow/config.yaml b/examples/flow/01_custom_flow/config.yaml deleted file mode 100644 index ce8691d..0000000 --- a/examples/flow/01_custom_flow/config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Configuration for custom flow example -flows: - greeting_flow: - type: "greeting" - config: - name: "greeting_flow" - prompt_templates_path: "flows/greeting_flow/prompts.yaml" - llm_blocks: - greeter: - model: "gpt-4o-mini" - temperature: 0.7 - max_tokens: 500 - api_key: ${OPENAI_API_KEY} diff --git a/examples/flow/01_custom_flow/flows/greeting_flow/__init__.py b/examples/flow/01_custom_flow/flows/greeting_flow/__init__.py deleted file mode 100644 index 5e21a8b..0000000 --- a/examples/flow/01_custom_flow/flows/greeting_flow/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Greeting flow module.""" - -from .flow import GreetingFlow, GreetingFlowConfig - -__all__ = ["GreetingFlow", "GreetingFlowConfig"] diff --git a/examples/flow/01_custom_flow/flows/greeting_flow/flow.py b/examples/flow/01_custom_flow/flows/greeting_flow/flow.py deleted file mode 100644 index d2e82d2..0000000 --- a/examples/flow/01_custom_flow/flows/greeting_flow/flow.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Simple greeting flow demonstrating custom flow creation.""" - -from typing import Any, Dict - -from quantmind.config.flows import BaseFlowConfig -from quantmind.config.llm import LLMConfig -from quantmind.config.registry import register_flow_config -from quantmind.flow.base import BaseFlow - - -@register_flow_config("greeting") -class GreetingFlowConfig(BaseFlowConfig): - """Configuration for greeting flow.""" - - def model_post_init(self, __context: Any) -> None: - """Initialize default configuration.""" - # First load prompt templates from path if specified - super().model_post_init(__context) - - if not self.llm_blocks: - self.llm_blocks = { - "greeter": LLMConfig( - model="gpt-4o-mini", temperature=0.7, max_tokens=500 - ) - } - - -class GreetingFlow(BaseFlow): - """A simple custom flow that greets users and provides suggestions.""" - - def run(self, user_input: Dict[str, Any]) -> Dict[str, str]: - """Execute the greeting flow. - - Args: - user_input: Dictionary containing 'user_name' and 'topic' - - Returns: - Dictionary with greeting and suggestions - """ - user_name = user_input.get("user_name", "there") - topic = user_input.get("topic", "learning") - - # Step 1: Generate greeting - greeter_llm = self._llm_blocks["greeter"] - - greeting_prompt = self._render_prompt( - "greeting_template", user_name=user_name, topic=topic - ) - - greeting = greeter_llm.generate_text(greeting_prompt) - - # Step 2: Generate follow-up suggestions - follow_up_prompt = self._render_prompt( - "follow_up_template", user_name=user_name, topic=topic - ) - - suggestions = greeter_llm.generate_text(follow_up_prompt) - - return { - "greeting": greeting or "Hello! Welcome to our system!", - "suggestions": suggestions or "Keep exploring and learning!", - } diff --git a/examples/flow/01_custom_flow/flows/greeting_flow/prompts.yaml b/examples/flow/01_custom_flow/flows/greeting_flow/prompts.yaml deleted file mode 100644 index 4507a28..0000000 --- a/examples/flow/01_custom_flow/flows/greeting_flow/prompts.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# Prompt templates for GreetingFlow -templates: - greeting_template: | - You are a friendly assistant. Greet the user and ask about their interest in {{ topic }}. - - User: {{ user_name }} - Topic: {{ topic }} - - Generate a personalized greeting. - - follow_up_template: | - Based on the user's name "{{ user_name }}" and their interest in {{ topic }}, - suggest 3 relevant next steps they might take. - - Be encouraging and specific. diff --git a/examples/flow/01_custom_flow/pipeline.py b/examples/flow/01_custom_flow/pipeline.py deleted file mode 100644 index c3f9e93..0000000 --- a/examples/flow/01_custom_flow/pipeline.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -"""Demo pipeline showing how to create and use a custom flow.""" - -from pathlib import Path - -# Import the custom flow -from flows.greeting_flow import GreetingFlow -from quantmind.config.settings import load_config - - -def main(): - """Run the custom flow demo.""" - print("=== Custom Flow Demo: Greeting Flow ===") - print() - - # Step 1: Load configuration from YAML using QuantMind settings - config_path = Path(__file__).parent / "config.yaml" - settings = load_config(config_path) - - # Step 2: Get the greeting flow configuration and convert to GreetingFlowConfig - config = settings.flows["greeting_flow"] - - print(f"📁 Flow name: {config.name}") - print(f"📁 Templates path: {config.prompt_templates_path}") - - print(f"✓ Loaded {len(config.prompt_templates)} prompt templates") - print(f"✓ Configured {len(config.llm_blocks)} LLM blocks") - print() - - # Step 3: Initialize the flow - try: - flow = GreetingFlow(config) - print("✓ Flow initialized successfully") - except Exception as e: - print(f"✗ Flow initialization failed: {e}") - print("This is expected without proper API configuration") - return - - # Step 4: Run the flow with sample data - user_inputs = [ - {"user_name": "Alice", "topic": "quantitative finance"}, - {"user_name": "Bob", "topic": "machine learning"}, - {"user_name": "Carol", "topic": "data science"}, - ] - - for user_input in user_inputs: - print(f"\n--- Processing: {user_input} ---") - try: - result = flow.run(user_input) - print("Greeting:", result.get("greeting", "N/A")) - print("Suggestions:", result.get("suggestions", "N/A")) - except Exception as e: - print(f"✗ Flow execution failed: {e}") - print("This is expected without proper API keys") - - print("\n=== Demo Complete ===") - print("\nKey takeaways from this example:") - print("• Simple flow configuration with dataclass") - print("• YAML-based prompt templates") - print("• Direct LLM block access (no wrapper methods)") - print("• Python-based orchestration logic") - print("• Easy to customize and extend") - - -if __name__ == "__main__": - main() diff --git a/examples/flow/02_summary_flow/.env.example b/examples/flow/02_summary_flow/.env.example deleted file mode 100644 index 52dba40..0000000 --- a/examples/flow/02_summary_flow/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -# Example environment file for QuantMind flows -# Copy this file to .env and fill in your actual API keys - -# OpenAI API Key (for GPT models) -OPENAI_API_KEY=sk-your-openai-api-key-here - -# Anthropic API Key (for Claude models) -ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key-here - -# Google API Key (for Gemini models) -GOOGLE_API_KEY=your-google-api-key-here - -# DeepSeek API Key (for DeepSeek models) -DEEPSEEK_API_KEY=your-deepseek-api-key-here - -# Custom environment variables (you can define your own) -MY_CUSTOM_LLM_KEY=your-custom-key-here diff --git a/examples/flow/02_summary_flow/README.md b/examples/flow/02_summary_flow/README.md deleted file mode 100644 index e3c933c..0000000 --- a/examples/flow/02_summary_flow/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# Summary Flow Example - -This example demonstrates the built-in SummaryFlow with custom chunking strategy and automatic API key management. - -## Structure - -```bash -examples/flow/02_summary_flow/ -├── flows/ -│ └── summary_flow/ -│ ├── __init__.py # Module exports -│ ├── prompts.yaml # Prompt templates -│ └── flow.py # Mock LLM implementation (for testing) -├── config.yaml # Flow configuration -├── mock_data.py # Sample financial papers -├── pipeline.py # Entry point -├── .env.example # Example .env file -└── README.md # This file -``` - -## Key Components - -### 1. Built-in SummaryFlow - -- Two-stage summarization: cheap model for chunks → powerful model for combination -- Flexible chunking strategies: size-based, custom, or disabled -- Cost-optimized mixture mode - -### 2. Custom Chunking Strategy - -- Demonstrates `ChunkingStrategy.BY_CUSTOM` -- User-defined chunking function (paragraph-based in demo) -- Runtime configuration update - -### 3. Automatic API Key Management - -- Environment variable resolution from `.env` file -- Smart provider inference (OpenAI models use `OPENAI_API_KEY`) -- Secure configuration without hardcoded keys - -## Easy Import Structure - -With the new `__init__.py` file, you can import the demo flow cleanly: - -```python -# Import from flow directory -from flows.summary_flow import DemoSummaryFlow - -# Use in pipelines -flow = DemoSummaryFlow(config) -``` - -## Running the Demo - -```bash -cd examples/flow/02_summary_flow -# Prepare the api-key .env file -cp ../../../.env.example .env -# Edit .env with your actual API keys -python pipeline.py -``` - -## Features Demonstrated - -1. **Custom Chunking**: Paragraph-based instead of size-based splitting -2. **Two-stage Processing**: Cost-effective bulk processing + high-quality synthesis -3. **API Key Resolution**: Automatic environment variable discovery -4. **Template Separation**: YAML-based prompts for easy editing -5. **Type-safe Configuration**: Proper `SummaryFlowConfig` loading - -## Configuration - -```yaml -# config.yaml -flows: - summary_demo: - type: "summary" # Uses built-in SummaryFlow - config: - name: "summary_flow" - prompt_templates_path: "flows/summary_flow/prompts.yaml" - use_chunking: true - chunk_size: 1000 -``` - -## Benefits Demonstrated - -1. **Smart Resource Usage**: Cheap model for bulk work, powerful model for synthesis -2. **Flexible Chunking**: Easy to implement custom splitting strategies -3. **Secure Configuration**: Environment variable-based API key management -4. **Simple Integration**: Built-in flow with minimal configuration -5. **Template Flexibility**: External YAML prompt templates - -This showcases the framework's principle: **powerful built-in flows with flexible customization options**. diff --git a/examples/flow/02_summary_flow/config.yaml b/examples/flow/02_summary_flow/config.yaml deleted file mode 100644 index 7521362..0000000 --- a/examples/flow/02_summary_flow/config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Configuration for summary flow demo -flows: - summary_demo: - type: "summary" - config: - name: "summary_flow" - prompt_templates_path: "flows/summary_flow/prompts.yaml" - use_chunking: true - chunk_size: 1000 # Smaller chunks for demo diff --git a/examples/flow/02_summary_flow/flows/summary_flow/__init__.py b/examples/flow/02_summary_flow/flows/summary_flow/__init__.py deleted file mode 100644 index 436ca0f..0000000 --- a/examples/flow/02_summary_flow/flows/summary_flow/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Summary flow module.""" - -from .flow import DemoSummaryFlow - -__all__ = ["DemoSummaryFlow"] diff --git a/examples/flow/02_summary_flow/flows/summary_flow/flow.py b/examples/flow/02_summary_flow/flows/summary_flow/flow.py deleted file mode 100644 index ddbc18f..0000000 --- a/examples/flow/02_summary_flow/flows/summary_flow/flow.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Summary flow with mock LLM for demonstration.""" - -from typing import List -from quantmind.config.flows import SummaryFlowConfig -from quantmind.flow.summary_flow import SummaryFlow - - -class MockLLMBlock: - """Mock LLM block for demonstration purposes.""" - - def __init__(self, model_name: str): - self.model_name = model_name - - def generate_text(self, prompt: str) -> str: - """Generate mock responses based on model type.""" - if ( - "cheap_summarizer" in self.model_name - or "gpt-4o-mini" in self.model_name - ): - # Mock cheap model response - shorter, more focused - if "methodology" in prompt.lower(): - return "This section discusses ML algorithms for financial prediction using historical data." - elif "results" in prompt.lower(): - return ( - "The model achieved 67% accuracy with Sharpe ratio of 1.8." - ) - else: - return "Key findings: Machine learning shows promise for financial applications." - - elif ( - "powerful_combiner" in self.model_name - or "gpt-4o" in self.model_name - ): - # Mock powerful model response - comprehensive combination - if "combine" in prompt.lower() or "summaries" in prompt.lower(): - return ( - "## Comprehensive Summary\n\n" - "This research demonstrates the successful application of machine learning " - "techniques in quantitative finance. The study employs various ML algorithms " - "including random forests and neural networks to predict market movements.\n\n" - "**Key Achievements:**\n" - "- Achieved 67% directional prediction accuracy\n" - "- Sharpe ratio improvement to 1.8 vs 1.2 baseline\n" - "- Demonstrated superiority over traditional statistical models\n\n" - "**Methodology:** The approach combines technical indicators with fundamental " - "analysis metrics, processed through ensemble learning methods.\n\n" - "**Implications:** These results suggest significant potential for ML-driven " - "trading strategies in institutional finance applications." - ) - else: - return ( - "This comprehensive analysis of machine learning applications in finance " - "demonstrates significant improvements over traditional approaches, with " - "practical implications for algorithmic trading and risk management." - ) - - return "Mock response generated successfully." - - -class DemoSummaryFlow(SummaryFlow): - """Summary flow with mock LLM blocks for demonstration.""" - - def _initialize_llm_blocks(self, llm_configs): - """Override to use mock LLM blocks.""" - llm_blocks = {} - for identifier, llm_config in llm_configs.items(): - # Create mock LLM blocks instead of real ones - mock_llm = MockLLMBlock(f"{identifier}_{llm_config.model}") - llm_blocks[identifier] = mock_llm - print( - f"✓ Initialized mock LLM '{identifier}' with model: {llm_config.model}" - ) - - return llm_blocks diff --git a/examples/flow/02_summary_flow/flows/summary_flow/prompts.yaml b/examples/flow/02_summary_flow/flows/summary_flow/prompts.yaml deleted file mode 100644 index 4459b2d..0000000 --- a/examples/flow/02_summary_flow/flows/summary_flow/prompts.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# Prompt templates for SummaryFlow -templates: - summarize_chunk_template: | - You are a financial research expert. Summarize the following content chunk - focusing on key insights, methodology, and findings. Keep it concise but comprehensive. - - Content: - {{ chunk_text }} - - Summary: - - combine_summaries_template: | - You are a financial research expert. Combine the following chunk summaries - into a coherent, comprehensive final summary. Eliminate redundancy and - create a well-structured overview. - - Chunk Summaries: - {{ summaries }} - - Final Summary: diff --git a/examples/flow/02_summary_flow/mock_data.py b/examples/flow/02_summary_flow/mock_data.py deleted file mode 100644 index cdcccdd..0000000 --- a/examples/flow/02_summary_flow/mock_data.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Mock financial research papers for demonstration.""" - -from quantmind.models.content import KnowledgeItem - - -def get_sample_papers(): - """Get sample financial research papers for testing.""" - # Large paper that will be chunked - large_paper = KnowledgeItem( - title="Machine Learning Applications in Algorithmic Trading: A Comprehensive Study", - abstract=( - "This paper presents a comprehensive analysis of machine learning techniques " - "applied to algorithmic trading strategies. We evaluate various ML models " - "including random forests, gradient boosting, and deep neural networks on " - "high-frequency trading data from major equity markets." - ), - content=( - "Introduction:\n" - "The financial markets have undergone significant transformation with the advent " - "of algorithmic trading. Machine learning techniques offer unprecedented " - "opportunities to identify complex patterns in market data that traditional " - "statistical methods might miss. This study focuses on the practical application " - "of ML algorithms in creating profitable trading strategies.\n\n" - "Literature Review:\n" - "Previous research in this domain has shown mixed results. Early studies by " - "Johnson et al. (2018) demonstrated modest improvements using linear models, " - "while more recent work by Chen and Liu (2021) achieved breakthrough results " - "with deep learning architectures. However, most studies lack comprehensive " - "evaluation across different market conditions and asset classes.\n\n" - "Methodology:\n" - "Our experimental setup involves training multiple ML models on 5 years of " - "high-frequency data from S&P 500 constituents. We employ a rolling window " - "approach with 252-day training periods and 63-day testing periods. Feature " - "engineering includes technical indicators (RSI, MACD, Bollinger Bands), " - "fundamental metrics (P/E ratios, earnings growth), and market microstructure " - "variables (bid-ask spreads, order book depth).\n\n" - "Model Architecture:\n" - "We implement three primary model types: (1) Random Forest with 1000 trees " - "and maximum depth of 10, (2) Gradient Boosting with learning rate 0.01 and " - "500 estimators, and (3) LSTM neural network with 128 hidden units and dropout " - "regularization. Each model is optimized using grid search cross-validation.\n\n" - "Results:\n" - "The experimental results demonstrate significant improvements over baseline " - "strategies. The ensemble approach combining all three models achieved 67% " - "directional accuracy compared to 52% for random walk baseline. Risk-adjusted " - "returns measured by Sharpe ratio improved from 1.2 to 1.8. Maximum drawdown " - "was reduced from 15% to 8%, indicating better risk management.\n\n" - "Statistical Analysis:\n" - "We conducted rigorous statistical testing to ensure result significance. " - "Bootstrap confidence intervals show 95% confidence that true accuracy lies " - "between 64-70%. Paired t-tests confirm statistical significance (p < 0.001) " - "for performance improvements. Out-of-sample testing on 2023 data validates " - "model robustness across different market regimes.\n\n" - "Risk Management:\n" - "Implementation includes comprehensive risk controls: position sizing based on " - "volatility targeting, correlation limits to prevent over-concentration, and " - "dynamic stop-loss levels adjusted for market volatility. These controls " - "contributed significantly to the improved risk-adjusted performance.\n\n" - "Conclusion:\n" - "This study demonstrates that machine learning techniques can significantly " - "enhance algorithmic trading performance when properly implemented with " - "appropriate risk controls. The key success factors include comprehensive " - "feature engineering, ensemble modeling approaches, and robust validation " - "methodologies. Future research should explore alternative data sources " - "and investigate model interpretability for regulatory compliance." - ), - authors=["Dr. Sarah Chen", "Prof. Michael Rodriguez", "Dr. Alex Kim"], - categories=["q-fin.TR", "q-fin.ST", "cs.AI"], - tags=[ - "machine learning", - "algorithmic trading", - "quantitative finance", - "risk management", - ], - source="demo_data", - ) - - # Small paper that won't be chunked - small_paper = KnowledgeItem( - title="High-Frequency Trading Impact on Market Liquidity", - abstract=( - "This study examines the impact of high-frequency trading on market liquidity " - "using transaction-level data from NYSE and NASDAQ." - ), - content=( - "This research analyzes high-frequency trading effects on market quality metrics. " - "Using millisecond-level data, we find that HFT improves bid-ask spreads by " - "12% on average but increases volatility during stress periods. The net effect " - "on market welfare depends on trading volume and market conditions." - ), - authors=["Dr. Jennifer Wang"], - categories=["q-fin.TR"], - tags=[ - "high-frequency trading", - "market liquidity", - "market microstructure", - ], - source="demo_data", - ) - - return [large_paper, small_paper] - - -def get_custom_chunking_example(): - """Get example with custom chunking strategy.""" - - def paragraph_chunker(text: str): - """Custom chunker that splits by paragraphs.""" - paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()] - return paragraphs - - paper = KnowledgeItem( - title="Custom Chunking Strategy Demo", - abstract="Demonstrates paragraph-based chunking instead of size-based.", - content=( - "First paragraph discusses the introduction to the topic.\n\n" - "Second paragraph covers the methodology used in the research.\n\n" - "Third paragraph presents the main results and findings.\n\n" - "Fourth paragraph concludes with implications and future work." - ), - authors=["Demo Author"], - categories=["demo"], - tags=["custom chunking"], - source="demo_data", - ) - - return paper, paragraph_chunker diff --git a/examples/flow/02_summary_flow/pipeline.py b/examples/flow/02_summary_flow/pipeline.py deleted file mode 100644 index e7eb638..0000000 --- a/examples/flow/02_summary_flow/pipeline.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env python3 -"""Demo pipeline for SummaryFlow with custom chunking strategy.""" - -from pathlib import Path - -from mock_data import get_custom_chunking_example - -from quantmind.config.settings import load_config -from quantmind.flow.summary_flow import SummaryFlow - - -def main(): - """Run the summary flow demo with custom chunking.""" - print("=== Summary Flow Demo: Custom Chunking ===") - print() - - # Step 1: Load configuration from YAML using QuantMind settings - config_path = Path(__file__).parent / "config.yaml" - settings = load_config(config_path) - - # Step 2: Get the summary flow configuration - config = settings.flows["summary_demo"] - - print(f"📁 Flow name: {config.name}") - print(f"📁 Templates path: {config.prompt_templates_path}") - print(f"✓ Loaded {len(config.prompt_templates)} prompt templates") - print(f"✓ Configured {len(config.llm_blocks)} LLM blocks") - print() - - # Step 3: Get sample paper with custom chunking strategy - paper, custom_chunker = get_custom_chunking_example() - - print(f"📄 Paper: '{paper.title}' ({len(paper.content)} chars)") - print(f"🧩 Custom chunker: {custom_chunker.__name__}") - print() - - # Step 4: Update config to use custom chunking - from quantmind.config.flows import ChunkingStrategy - - config.chunk_strategy = ChunkingStrategy.BY_CUSTOM - config.chunk_custom_strategy = custom_chunker - - print(f"⚙️ Chunking: {config.use_chunking}") - print(f"⚙️ Strategy: {config.chunk_strategy.value}") - print(f"⚙️ Custom function: {config.chunk_custom_strategy.__name__}") - print() - - # Step 5: Initialize and run the flow - try: - flow = SummaryFlow(config) - print("✓ Flow initialized successfully") - - # Show LLM blocks configuration - print("\n🤖 LLM Configuration:") - for name, llm_config in config.llm_blocks.items(): - key_status = "✓ Found" if llm_config.api_key else "✗ Missing" - print(f" • {name}: {llm_config.model} ({key_status})") - print() - - # Run the flow - print("🚀 Running summary flow...") - result = flow.run(paper) - - print(f"✓ Generated summary ({len(result)} chars)") - print("\n" + "=" * 50) - print("📝 SUMMARY:") - print("=" * 50) - print(result) - print("=" * 50) - - except Exception as e: - print(f"✗ Flow execution failed: {e}") - print("💡 Make sure you have set up API keys in .env file") - - print("\n=== Demo Complete ===") - print("\nKey features demonstrated:") - print("• Custom chunking strategy implementation") - print("• Two-stage summarization (cheap + powerful LLMs)") - print("• Automatic API key resolution from environment") - print("• YAML-based prompt templates") - print("• Type-safe configuration loading") - - -if __name__ == "__main__": - main() diff --git a/examples/flow/03_podcast_flow/README.md b/examples/flow/03_podcast_flow/README.md deleted file mode 100644 index 9e55f42..0000000 --- a/examples/flow/03_podcast_flow/README.md +++ /dev/null @@ -1,110 +0,0 @@ -# Podcast Flow Examples - -This directory contains examples demonstrating how to use the `PodcastFlow` class to generate podcast scripts from summary input, following the same structure as other custom flows in this project. - -## Structure - -``` -03_podcast_flow/ -├── README.md # This documentation file -├── pipeline.py # Main demo pipeline -├── config.yaml # Configuration file -└── flows/ - └── podcast_flow/ - ├── __init__.py # Package initialization - ├── flow.py # Main flow implementation - └── prompts.yaml # Prompt templates -``` - -## Quick Start - -```bash -# Run the demo pipeline -python pipeline.py - -# Or import and use in your code -from flows.podcast_flow.flow import PodcastFlow -``` - -## Configuration - -The flow is configured through `config.yaml`: - -```yaml -flows: - podcast_flow: - type: "podcast" - config: - name: "podcast_flow" - prompt_templates_path: "flows/podcast_flow/prompts.yaml" - llm_blocks: - main_generator: - model: "gpt-4o-mini" - temperature: 0.5 - max_tokens: 1000 -``` - -## Usage - -```python -from flows.podcast_flow.flow import PodcastFlow -from quantmind.config.settings import load_config - -# Load configuration -settings = load_config("config.yaml") -config = settings.flows["podcast_flow"] - -# Create and run flow -flow = PodcastFlow(config) -script = flow.run( - summary="Your summary text here..." -) -``` - -## Features - -- **Flexible Input**: Accepts summary text and optional intro/outro hints -- **LLM Integration**: Uses configured LLM blocks for content generation -- **Template System**: Supports customizable prompt templates via YAML -- **Structured Output**: Returns organized script sections (intro, main, outro) -- **Fallback Support**: Gracefully handles missing LLM blocks or templates - -## Output Format - -The flow returns a dictionary with: -```python -{ - "main": "Generated main content...", -} -``` - -## Prompt Templates - -The flow uses three main prompt templates: - -1. **intro_prompt**: Generates engaging podcast introductions -2. **main_prompt**: Converts summaries into conversational podcast content -3. **outro_prompt**: Creates effective podcast conclusions - -## Environment Setup - -Set your API keys as environment variables: -```bash -export OPENAI_API_KEY="your-openai-api-key" -``` - -## Examples - -The `pipeline.py` file demonstrates: -- Loading configuration from YAML -- Initializing the flow -- Running with multiple sample inputs -- Error handling for missing API keys - -## Key Takeaways - -- **Consistent Structure**: Follows the same pattern as other custom flows -- **YAML Configuration**: Easy to modify without changing code -- **Template System**: Flexible prompt management -- **Error Handling**: Graceful fallbacks for missing resources -- **Extensible**: Easy to customize for different podcast styles diff --git a/examples/flow/03_podcast_flow/config.yaml b/examples/flow/03_podcast_flow/config.yaml deleted file mode 100644 index 09f19f6..0000000 --- a/examples/flow/03_podcast_flow/config.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Configuration for podcast flow example -flows: - podcast_flow: - type: "podcast" - config: - name: "podcast_flow" - prompt_templates_path: "flows/podcast_flow/prompts.yaml" - llm_blocks: - main_generator: - model: "gpt-4o-mini" - temperature: 0.5 - max_tokens: 1000 - api_key: ${OPENAI_API_KEY} - num_speakers: 2 - speaker_languages: "en-us" - summary_hint: "" diff --git a/examples/flow/03_podcast_flow/flows/podcast_flow/__init__.py b/examples/flow/03_podcast_flow/flows/podcast_flow/__init__.py deleted file mode 100644 index a3d80b7..0000000 --- a/examples/flow/03_podcast_flow/flows/podcast_flow/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Podcast Flow Package. - -This package contains the implementation of the PodcastFlow -for generating podcast scripts from summary input. -""" - -from .flow import CustomizedPodcastFlow - -__all__ = ["CustomizedPodcastFlow"] diff --git a/examples/flow/03_podcast_flow/flows/podcast_flow/flow.py b/examples/flow/03_podcast_flow/flows/podcast_flow/flow.py deleted file mode 100644 index bfca8d2..0000000 --- a/examples/flow/03_podcast_flow/flows/podcast_flow/flow.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Podcast flow with mock LLM for demonstration.""" - -from typing import Dict, Any -from venv import create -from quantmind.config.flows import PodcastFlowConfig -from quantmind.flow.podcast_flow import PodcastFlow -from quantmind.llm.block import LLMBlock, create_llm_block - - -class CustomizedPodcastFlowConfig(PodcastFlowConfig): - """Configuration for the PodcastFlow with LLM blocks.""" - - num_speakers: int = 2 - speaker_languages: str = "en-us" - summary_hint: str = "This is a sample summary hint for the podcast." - - -class CustomizedPodcastFlow(PodcastFlow): - """Podcast flow with mock LLM blocks for demonstration.""" - - def _initialize_llm_blocks(self, llm_configs): - """Override to use mock LLM blocks.""" - llm_blocks = {} - for identifier, llm_config in llm_configs.items(): - # Create mock LLM blocks instead of real ones - llm_block = create_llm_block(llm_config) - llm_blocks[identifier] = llm_block - print( - f"✓ Initialized mock LLM '{identifier}' with model: {llm_config.model}" - ) - - return llm_blocks diff --git a/examples/flow/03_podcast_flow/flows/podcast_flow/prompts.yaml b/examples/flow/03_podcast_flow/flows/podcast_flow/prompts.yaml deleted file mode 100644 index 3dd4949..0000000 --- a/examples/flow/03_podcast_flow/flows/podcast_flow/prompts.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# Prompt templates for PodcastFlow -templates: - main_prompt: | - You are a professional podcast script writer. Convert into engaging podcast content based on this summary. - - Summary: {{ summary_hint }} - - Generate the main podcast content: diff --git a/examples/flow/03_podcast_flow/pipeline.py b/examples/flow/03_podcast_flow/pipeline.py deleted file mode 100644 index 974fb2f..0000000 --- a/examples/flow/03_podcast_flow/pipeline.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -"""Demo pipeline showing how to create and use a podcast flow.""" - -import json -from pathlib import Path - -# Import the custom flow -from flows.podcast_flow.flow import PodcastFlow - -from quantmind.config.settings import load_config - - -def main(): - """Run the podcast flow demo.""" - print("=== Custom Flow Demo: Podcast Flow ===") - print() - - # Step 1: Load configuration from YAML using QuantMind settings - config_path = Path(__file__).parent / "config.yaml" - settings = load_config(config_path) - - # Step 2: Get the podcast flow configuration and convert to PodcastFlowConfig - config = settings.flows["podcast_flow"] - - print(f"📁 Flow name: {config.name}") - print(f"📁 Templates path: {config.prompt_templates_path}") - - print(f"✓ Loaded {len(config.prompt_templates)} prompt templates") - print(f"✓ Configured {len(config.llm_blocks)} LLM blocks") - print() - - # Step 3: Initialize the flow - try: - flow = PodcastFlow(config) - print("✓ Flow initialized successfully") - except Exception as e: - print(f"✗ Flow initialization failed: {e}") - print("This is expected without proper API configuration") - return - - # Step 4: Run the flow with sample data - sample_inputs = [ - { - "summary": "Artificial Intelligence is transforming healthcare in unprecedented ways. Machine learning algorithms can now diagnose diseases with accuracy rates exceeding human doctors. AI-powered imaging systems detect early-stage cancers that might be missed by traditional methods.", - } - ] - - for i, input_data in enumerate(sample_inputs, 1): - print(f"\n--- Processing Podcast {i}: {input_data['summary']} ---") - try: - result = flow.run(summary=input_data["summary"]) - assert isinstance( - result, dict - ), "Flow output should be a dictionary" - with open(f"podcast_script_{i}.json", "w") as f: - json.dump(result, f, indent=4) - print(f"✓ Podcast script saved to podcast_script_{i}.json") - except Exception as e: - print(f"✗ Flow execution failed: {e}") - print("This is expected without proper API keys") - - print("\n=== Demo Complete ===") - print("\nKey takeaways from this example:") - print("• Podcast flow configuration with dataclass") - print("• YAML-based prompt templates for main") - print("• Multiple LLM blocks for different content types") - print("• Python-based orchestration logic") - print("• Easy to customize and extend for different podcast styles") - - -if __name__ == "__main__": - main() diff --git a/examples/flow/README.md b/examples/flow/README.md deleted file mode 100644 index 056cf32..0000000 --- a/examples/flow/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Flow Examples - -This directory contains examples demonstrating the new QuantMind flow architecture. - -## Examples Overview - -### [01_custom_flow](./01_custom_flow/) - Simple Custom Flow - -Learn how to create a custom flow from scratch: - -- Basic flow configuration with Pydantic models -- YAML-based prompt templates -- Python-based orchestration logic -- Direct LLM block access - -**Key Learning**: How to implement custom business logic using the new architecture. - -### [02_summary_flow](./02_summary_flow/) - Built-in Summary Flow Demo - -Explore the built-in SummaryFlow with various configurations: - -- Chunking strategies (size-based, custom, disabled) -- Mixture mode (cheap + powerful LLMs) -- Mock LLM implementation for testing -- Real financial research paper examples - -**Key Learning**: How to leverage and configure built-in flows for optimal cost/quality trade-offs. - -### [03_podcast_flow](./03_podcast_flow/) - Built-in Podcast Flow Demo - -Explore the built-in PodcastFlow with various configurations: - -- Mixture mode (intro, main, outro) -- Mock LLM implementation for testing -- Real podcast script examples in various domains - -**Key Learning**: How to leverage and configure built-in flows for optimal script in specific domain. - -## Architecture Principles Demonstrated - -Both examples showcase the core principles of the new flow architecture: - -1. **Resource-Based Configuration**: Config defines resources (LLM blocks, templates), not orchestration logic -2. **Code-Based Orchestration**: Business logic implemented in Python for maximum flexibility -3. **Direct Access**: No unnecessary wrapper methods, just direct access to resources -4. **Template Separation**: Prompts in YAML files for easy editing and maintenance -5. **Type Safety**: Pydantic-based configuration instead of complex schemas -6. **Easy Imports**: Each flow directory has `__init__.py` for clean imports (e.g., `from flows.greeting_flow import GreetingFlow`) - -## Quick Start - -Choose the example that matches your needs: - -- **Want to create a custom flow?** → Start with `01_custom_flow` -- **Want to use built-in flows?** → Start with `02_summary_flow` - -Each example is self-contained and includes: - -- Complete working code -- Mock implementations (no API keys required) -- Comprehensive documentation -- Clear explanations of benefits - -## Running Examples - -Each example can be run independently: - -```bash -# Custom flow example -cd 01_custom_flow -# Prepare the api-key .env file -cp .env.example .env -python pipeline.py -``` - -```bash -# Summary flow example -cd 02_summary_flow -# Prepare the api-key .env file -cp .env.example .env -python pipeline.py -``` - -```bash -# Podcast flow example -cd 03_podcast_flow -# Prepare the api-key .env file -cp .env.example .env -python pipeline.py -``` - -Both examples work without API keys by using mock LLM implementations that demonstrate the flow logic and architecture benefits. diff --git a/examples/llm/embedding_block_example.py b/examples/llm/embedding_block_example.py deleted file mode 100644 index ec477c7..0000000 --- a/examples/llm/embedding_block_example.py +++ /dev/null @@ -1,205 +0,0 @@ -"""Example usage of EmbeddingBlock for different embedding providers.""" - -import os -from typing import List - -from quantmind.config import EmbeddingConfig -from quantmind.llm import EmbeddingBlock, create_embedding_block -from quantmind.utils.logger import get_logger - -logger = get_logger(__name__) - - -def example_openai_embeddings(): - """Example using OpenAI embeddings.""" - print("\n=== OpenAI Embeddings Example ===") - - # Configuration for OpenAI embeddings - config = EmbeddingConfig( - model="text-embedding-ada-002", - api_key=os.getenv("OPENAI_API_KEY"), - timeout=30, - encoding_format="float", - ) - - # Create embedding block - embedding_block = create_embedding_block(config) - - # Test connection - if embedding_block.test_connection(): - print("✅ OpenAI connection successful") - else: - print("❌ OpenAI connection failed") - return - - # Generate single embedding - text = "This is a sample text for embedding generation." - embedding = embedding_block.generate_embedding(text) - - if embedding: - print(f"✅ Generated embedding with {len(embedding)} dimensions") - print(f" First 5 values: {embedding[:5]}") - - # Generate multiple embeddings - texts = [ - "First sample text for embedding.", - "Second sample text with different content.", - "Third sample text for batch processing.", - ] - - embeddings = embedding_block.generate_embeddings(texts) - - if embeddings: - print(f"✅ Generated {len(embeddings)} embeddings") - for i, emb in enumerate(embeddings): - print(f" Text {i + 1}: {len(emb)} dimensions") - - # Get embedding information - info = embedding_block.get_info() - print(f"📊 Model info: {info['model']}") - print(f"📊 Provider: {info['provider']}") - - -def example_azure_embeddings(): - """Example using Azure OpenAI embeddings.""" - print("\n=== Azure OpenAI Embeddings Example ===") - - # Configuration for Azure OpenAI embeddings - config = EmbeddingConfig( - model="text-embedding-ada-002", - api_key=os.getenv("AZURE_API_KEY"), - api_base=os.getenv("AZURE_API_BASE"), - api_version=os.getenv("AZURE_API_VERSION", "2023-05-15"), - api_type="azure", - timeout=30, - encoding_format="float", - ) - - # Create embedding block - embedding_block = create_embedding_block(config) - - # Test connection - if embedding_block.test_connection(): - print("✅ Azure OpenAI connection successful") - else: - print("❌ Azure OpenAI connection failed") - return - - # Generate single embedding - text = "This is a sample text for Azure OpenAI embedding generation." - embedding = embedding_block.generate_embedding(text) - - if embedding: - print(f"✅ Generated embedding with {len(embedding)} dimensions") - print(f" First 5 values: {embedding[:5]}") - - # Generate multiple embeddings - texts = [ - "First sample text for Azure embedding.", - "Second sample text with different content.", - "Third sample text for batch processing.", - ] - - embeddings = embedding_block.generate_embeddings(texts) - - if embeddings: - print(f"✅ Generated {len(embeddings)} embeddings") - for i, emb in enumerate(embeddings): - print(f" Text {i + 1}: {len(emb)} dimensions") - - # Get embedding information - info = embedding_block.get_info() - print(f"📊 Model info: {info['model']}") - print(f"📊 Provider: {info['provider']}") - - -def example_configuration_variants(): - """Example showing different configuration variants.""" - print("\n=== Configuration Variants Example ===") - - # Base configuration - base_config = EmbeddingConfig( - model="text-embedding-ada-002", - api_key=os.getenv("OPENAI_API_KEY"), - encoding_format="float", - ) - - # Create variants with different parameters - fast_config = base_config.create_variant(timeout=10, retry_attempts=1) - - conservative_config = base_config.create_variant( - timeout=120, retry_attempts=5, retry_delay=2.0 - ) - - print(f"Base config timeout: {base_config.timeout}s") - print(f"Fast config timeout: {fast_config.timeout}s") - print(f"Conservative config timeout: {conservative_config.timeout}s") - - # Test with temporary configuration - embedding_block = create_embedding_block(base_config) - - with embedding_block.temporary_config(timeout=5): - print("Using temporary configuration with 5s timeout") - # Any embedding operations here will use the temporary config - embedding = embedding_block.generate_embedding("Test with temp config") - if embedding: - print("✅ Temporary configuration worked") - - -def example_error_handling(): - """Example showing error handling and fallbacks.""" - print("\n=== Error Handling Example ===") - - # Try with invalid API key - config = EmbeddingConfig( - model="text-embedding-ada-002", - api_key="invalid_key", - timeout=5, - ) - - embedding_block = create_embedding_block(config) - - # This should fail gracefully - embedding = embedding_block.generate_embedding("Test text") - if embedding is None: - print("✅ Gracefully handled invalid API key") - - # Try with non-existent model - config = EmbeddingConfig( - model="non-existent-model", - timeout=5, - ) - - try: - embedding_block = create_embedding_block(config) - print("❌ Should have failed with non-existent model") - except Exception as e: - print(f"✅ Gracefully handled non-existent model: {e}") - - -def main(): - """Run all embedding examples.""" - print("🚀 EmbeddingBlock Examples") - print("=" * 50) - - # Run examples based on available API keys - if os.getenv("OPENAI_API_KEY"): - example_openai_embeddings() - else: - print("\n⚠️ Skipping OpenAI examples - OPENAI_API_KEY not set") - - if os.getenv("AZURE_API_KEY") and os.getenv("AZURE_API_BASE"): - example_azure_embeddings() - else: - print( - "\n⚠️ Skipping Azure example - AZURE_API_KEY or AZURE_API_BASE not set" - ) - - example_configuration_variants() - example_error_handling() - - print("\n✅ All examples completed!") - - -if __name__ == "__main__": - main() diff --git a/examples/llm/llm_block_example.py b/examples/llm/llm_block_example.py deleted file mode 100644 index 0268e37..0000000 --- a/examples/llm/llm_block_example.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Example demonstrating the LLMBlock architecture.""" - -import os - -from dotenv import load_dotenv - -from quantmind.config import LLMConfig -from quantmind.llm import create_llm_block - -load_dotenv() - - -def example_basic_llm_block(): - """Example 1: Basic LLMBlock usage.""" - print("=== Example 1: Basic LLMBlock Usage ===") - - # Create LLM configuration - config = LLMConfig( - model="deepseek/deepseek-chat", # LiteLLM format - temperature=0.0, - max_tokens=1000, - api_key=os.getenv("DEEPSEEK_API_KEY"), - ) - - # Create LLMBlock - llm_block = create_llm_block(config) - - # Test connection - if llm_block.test_connection(): - print("✅ LLMBlock connection successful!") - - # Generate text - response = llm_block.generate_text("What is machine learning?") - print(f"Response: {response[:100]}...") - - # Get block info - print(f"Block info: {llm_block.get_info()}") - else: - print("❌ LLMBlock connection failed") - - -def example_advanced_features(): - """Example 5: Advanced features and configuration.""" - print("\n=== Example 5: Advanced Features ===") - - # Advanced configuration - config = LLMConfig( - model="gpt-4o", - temperature=0.7, - max_tokens=2000, - top_p=0.9, - timeout=120, - retry_attempts=5, - retry_delay=2.0, - system_prompt="You are a quantitative finance expert.", - custom_instructions="Always provide practical examples and code snippets.", - extra_params={"frequency_penalty": 0.1, "presence_penalty": 0.1}, - ) - - print(f"Advanced config: {config.model_dump()}") - - # Create LLMBlock - llm_block = create_llm_block(config) - - # Using context manager for temporary changes - with llm_block.temporary_config(temperature=0.0, max_tokens=500): - print("Inside temporary config context") - print(f"Current block info: {llm_block.get_info()}") - - print("Outside temporary config context") - print(f"Current block info: {llm_block.get_info()}") - - -if __name__ == "__main__": - print("🚀 QuantMind LLMBlock Architecture Examples") - print("=" * 50) - - # Run examples - example_basic_llm_block() - example_advanced_features() - - print("\n✅ All examples completed!") diff --git a/examples/memory/basic_memory_usage.py b/examples/memory/basic_memory_usage.py deleted file mode 100644 index 5642bf5..0000000 --- a/examples/memory/basic_memory_usage.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Minimal example showing how to build a memory timeline.""" - -from quantmind.brain.memory import Memory -from quantmind.models.memory import ActionStep, TaskStep, ToolCall -from quantmind.models.messages import ChatMessage, MessageRole -from quantmind.utils.monitoring import Timing, TokenUsage - - -def main(): - """Main function.""" - memory = Memory("You are a quantitative research assistant.") - - memory.steps.append(TaskStep(task="Gather the latest market sentiment.")) - - memory.steps.append( - ActionStep( - step_number=1, - timing=Timing(start_time=0.0, end_time=0.5), - model_input_messages=[ - ChatMessage( - role=MessageRole.USER, - content=[ - {"type": "text", "text": "Any updates on bond markets?"} - ], - ) - ], - tool_calls=[ - ToolCall( - name="fetch_sentiment", - arguments={"asset": "treasury", "lookback": "1d"}, - id="call-1", - ) - ], - model_output="Sentiment looks neutral across regions.", - observations="Tool returned neutral scores.", - token_usage=TokenUsage(input_tokens=12, output_tokens=9), - ) - ) - - print("Succinct steps:") - for step in memory.get_succinct_steps(): - print(step) - - print("\nMessages replay:") - messages = [] - messages.extend(memory.system_prompt.to_messages()) - for step in memory.steps: - messages.extend(step.to_messages()) - - # Define colors for different message roles - ROLE_COLORS = { - MessageRole.SYSTEM: "\033[35m", # Magenta - MessageRole.USER: "\033[32m", # Green - MessageRole.ASSISTANT: "\033[36m", # Cyan - MessageRole.TOOL_CALL: "\033[33m", # Yellow - MessageRole.TOOL_RESPONSE: "\033[34m", # Blue - } - RESET = "\033[0m" - - for message in messages: - color = ROLE_COLORS.get(message.role, "") - print(f"{color}[{message.role.value}]{RESET} {message.content}") - - -if __name__ == "__main__": - main() diff --git a/examples/parser/llama_parser_example.ipynb b/examples/parser/llama_parser_example.ipynb deleted file mode 100644 index 0caf201..0000000 --- a/examples/parser/llama_parser_example.ipynb +++ /dev/null @@ -1,115 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "from pathlib import Path\n", - "\n", - "from autoscholar.parser.llama_parser import LlamaParser, ParsingMode, ResultType" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "llama_parse_api = getpass.getpass(\"Enter your LlamaParse API key: \")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Started parsing the file under job_id 8842778c-ce0f-4c05-a295-51bdb80b9e1d\n" - ] - } - ], - "source": [ - "import nest_asyncio\n", - "\n", - "nest_asyncio.apply()\n", - "\n", - "test_pdf_path = Path(\"test-pdf.pdf\")\n", - "parser = LlamaParser(\n", - " api_key=llama_parse_api,\n", - " parsing_mode=ParsingMode.BALANCED,\n", - " result_type=ResultType.MD,\n", - ")\n", - "\n", - "result = parser.parse(test_pdf_path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "('# DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open '\n", - " 'Language Models\\n'\n", - " '\\n'\n", - " '# Zhihong Shao')\n", - "['img_p0_1.png']\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from io import BytesIO\n", - "from pprint import pprint\n", - "\n", - "from PIL import Image\n", - "\n", - "pprint(result.get_markdown_documents()[0].text[:100])\n", - "\n", - "pprint(result.get_image_names())\n", - "\n", - "Image.open(BytesIO(result.get_image_data(\"img_p0_1.png\")))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.16" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/examples/parser/simple_llama_parser.py b/examples/parser/simple_llama_parser.py deleted file mode 100644 index c7be9ca..0000000 --- a/examples/parser/simple_llama_parser.py +++ /dev/null @@ -1,306 +0,0 @@ -"""Simple LlamaParser usage example. - -This example shows basic usage of the LlamaParser for parsing -individual PDF files or URLs using the new Pydantic configuration system. -""" - -import logging -import os -from pathlib import Path - -from colorama import Fore, Style - -from quantmind.config.parsers import LlamaParserConfig, ParsingMode, ResultType -from quantmind.models.paper import Paper -from quantmind.parsers.llama_parser import LlamaParser - -# Set logging level to DEBUG -logging.basicConfig(level=logging.DEBUG) - - -def demo_file_parsing(): - """Demonstrate parsing a local PDF file.""" - print("=== File Parsing Demo ===\n") - - # Create parser with Pydantic configuration - config = LlamaParserConfig( - api_key=os.getenv("LLAMA_CLOUD_API_KEY") or "demo_key", - result_type=ResultType.MD, - parsing_mode=ParsingMode.FAST, - max_file_size_mb=25, - ) - parser = LlamaParser(config) - - # Example PDF path (would need to exist for real usage) - pdf_path = "./examples/parser/test-pdf.pdf" - - print(f"Parser configuration:") - print( - f"- Result type: {parser.llama_config.result_type if isinstance(parser.llama_config.result_type, str) else parser.llama_config.result_type.value}" - ) - print( - f"- Parsing mode: {parser.llama_config.parsing_mode if isinstance(parser.llama_config.parsing_mode, str) else parser.llama_config.parsing_mode.value}" - ) - print(f"- Max file size: {parser.llama_config.max_file_size_mb}MB") - print() - - if Path(pdf_path).exists(): - print(f"Parsing file: {pdf_path}") - try: - # Parse the file - content = parser.extract_from_file(pdf_path) - print( - Fore.GREEN - + f"Successfully parsed {len(content)} characters" - + Style.RESET_ALL - ) - print( - Fore.GREEN - + f"Content preview: {content[:200]}..." - + Style.RESET_ALL - ) - - except Exception as e: - print(Fore.RED + f"Parsing error: {e}" + Style.RESET_ALL) - else: - print(Fore.RED + f"Demo PDF not found at: {pdf_path}" + Style.RESET_ALL) - print( - Fore.YELLOW - + "In real usage, provide path to an existing PDF file." - + Style.RESET_ALL - ) - - print() - - -def demo_url_parsing(): - """Demonstrate parsing a PDF from URL.""" - print("=== URL Parsing Demo ===\n") - - config = LlamaParserConfig( - api_key=os.getenv("LLAMA_CLOUD_API_KEY") or "demo_key", - result_type=ResultType.TXT, - parsing_mode=ParsingMode.BALANCED, - ) - parser = LlamaParser(config) - - # Example PDF URL (ArXiv paper) - pdf_url = "https://arxiv.org/pdf/2301.00001.pdf" - - print(f"Parsing URL: {pdf_url}") - print("Note: This requires valid LLAMA_CLOUD_API_KEY") - - try: - # Parse from URL - content = parser.extract_from_url(pdf_url) - print( - Fore.GREEN - + f"Successfully parsed {len(content)} characters" - + Style.RESET_ALL - ) - print( - Fore.GREEN - + f"Content preview: {content[:200]}..." - + Style.RESET_ALL - ) - - except Exception as e: - print( - Fore.RED + f"Expected error without API key: {e}" + Style.RESET_ALL - ) - - print() - - -def demo_paper_parsing(): - """Demonstrate parsing a Paper object.""" - print("=== Paper Object Parsing Demo ===\n") - - # Create a Paper object - paper = Paper( - paper_id="2301.00001", - title="Example Financial Research Paper", - authors=["Author One", "Author Two"], - abstract="This paper demonstrates quantitative methods...", - pdf_url="https://arxiv.org/pdf/2301.00001.pdf", - categories=["q-fin.ST"], - ) - - # Create parser with advanced configuration - config = LlamaParserConfig( - api_key=os.getenv("LLAMA_CLOUD_API_KEY") or "demo_key", - result_type=ResultType.MD, - parsing_mode=ParsingMode.PREMIUM, - system_prompt=( - "Extract key findings, methodology, and quantitative " - "results from this financial research paper." - ), - system_prompt_append=( - "Focus on statistical methods and financial metrics." - ), - ) - parser = LlamaParser(config) - - print(f"Paper: {paper.title}") - print(f"Authors: {', '.join(paper.authors)}") - print(f"PDF URL: {paper.pdf_url}") - print() - - print("Parser configuration:") - info = parser.get_parser_info() - for key, value in info.items(): - if key != "config": # Skip detailed config - print(f"- {key}: {value}") - print() - - try: - # Parse the paper - enriched_paper = parser.parse_paper(paper) - - if enriched_paper.content: - print(Fore.GREEN + f"Parsing successful!" + Style.RESET_ALL) - print( - Fore.GREEN - + f"Content length: {len(enriched_paper.content)} characters" - + Style.RESET_ALL - ) - print( - Fore.GREEN - + f"Parser info: {enriched_paper.meta_info.get('parser_info', {})}" - + Style.RESET_ALL - ) - else: - print( - Fore.RED - + "No content extracted (expected without API key)" - + Style.RESET_ALL - ) - - except Exception as e: - print( - Fore.RED + f"Expected error without API key: {e}" + Style.RESET_ALL - ) - - print() - - -def demo_configuration_options(): - """Demonstrate different configuration options.""" - print("=== Configuration Options Demo ===\n") - - # Get API key once for all configurations - api_key = os.getenv("LLAMA_CLOUD_API_KEY") or "demo_key" - - configurations = [ - { - "name": "Fast Text Extraction", - "config": LlamaParserConfig( - api_key=api_key, - result_type=ResultType.TXT, - parsing_mode=ParsingMode.FAST, - max_file_size_mb=50, - ), - }, - { - "name": "Balanced Markdown", - "config": LlamaParserConfig( - api_key=api_key, - result_type=ResultType.MD, - parsing_mode=ParsingMode.BALANCED, - max_file_size_mb=25, - ), - }, - { - "name": "Premium with Custom Prompts", - "config": LlamaParserConfig( - api_key=api_key, - result_type=ResultType.MD, - parsing_mode=ParsingMode.PREMIUM, - max_file_size_mb=15, - system_prompt="Extract financial data and analysis", - system_prompt_append="Include all numerical results", - ), - }, - ] - - for config_info in configurations: - print(f"Configuration: {config_info['name']}") - try: - parser = LlamaParser(config_info["config"]) - info = parser.get_parser_info() - print(f"- Result type: {info['result_type']}") - print(f"- Parsing mode: {info['parsing_mode']}") - print(f"- Max file size: {info['max_file_size_mb']}MB") - if info.get("system_prompt"): - print(f"- Custom prompt: Yes") - print() - - except Exception as e: - print(Fore.RED + f"Configuration error: {e}" + Style.RESET_ALL) - print() - - -def main(): - """Run all demonstration examples.""" - print("QuantMind LlamaParser: Simple Usage Examples") - print("=" * 45) - print() - - # Check for API key - try: - api_key = os.getenv("LLAMA_CLOUD_API_KEY") - if api_key: - print("✅ LLAMA_CLOUD_API_KEY found - examples will use real API") - else: - print("⚠️ LLAMA_CLOUD_API_KEY not set - running in demo mode") - print( - " 💡 Tip: Create a .env file with LLAMA_CLOUD_API_KEY=your_key" - ) - print(" Or set as environment variable for full functionality") - except Exception as e: - print(f"⚠️ Error loading API key: {e}") - print(" Running in demo mode") - print() - - # Run demonstrations - demo_configuration_options() - demo_file_parsing() - demo_url_parsing() - demo_paper_parsing() - - print("=" * 45) - print(Fore.GREEN + "Examples completed!" + Style.RESET_ALL) - print("\nNext steps:") - print( - Fore.YELLOW + "1. 🔑 Set up API key for real parsing:" + Style.RESET_ALL - ) - print( - Fore.YELLOW - + " • Create .env file: LLAMA_CLOUD_API_KEY=your_actual_key" - + Style.RESET_ALL - ) - print( - Fore.YELLOW - + " • Or set environment variable: export LLAMA_CLOUD_API_KEY=your_key" - + Style.RESET_ALL - ) - print(Fore.YELLOW + "2. 📄 Try with your own PDF files" + Style.RESET_ALL) - print( - Fore.YELLOW - + "3. ⚙️ Experiment with different parsing modes" - + Style.RESET_ALL - ) - print( - Fore.YELLOW - + "4. 🔄 See arxiv_llama_pipeline.py for full workflow integration" - + Style.RESET_ALL - ) - print( - Fore.YELLOW - + "\n💡 Create a .env file with: LLAMA_CLOUD_API_KEY=your_api_key_here" - + Style.RESET_ALL - ) - - -if __name__ == "__main__": - main() diff --git a/examples/parser/test-pdf.pdf b/examples/parser/test-pdf.pdf deleted file mode 100644 index 91d8281a425619e1cab7f93f2d05cf4d318f84f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173133 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%0Aaw8SA~A+fPMyodm7{?Z`kJ6cb05 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 List[Dict[str, str]]: - """Execute the QA generation flow. - - Args: - document: KnowledgeItem (typically a Paper) to generate questions for - - Returns: - List of dictionaries containing question data - """ - logger.info(f"Starting QA flow for: {document.title}") - - content = document.content or "" - if not content: - logger.warning("No content available for QA generation") - return [] - - # Step 1: Generate initial questions - initial_questions = self._generate_initial_questions(document) - if not initial_questions: - logger.error("Failed to generate initial questions") - return [] - - # Step 2: Refine and categorize questions - refined_questions = self._refine_questions(initial_questions, document) - - # Step 3: Store in meta_info following best practice - qa_metadata = { - "questions": refined_questions, - "generated_at": datetime.now().isoformat(), - "model_used": self.config.llm_blocks["question_generator"].model, - "question_count": len(refined_questions), - "focus_areas": self.config.focus_areas, - "depth_level": self.config.question_depth, - } - - document.meta_info["qa_data"] = qa_metadata - logger.info( - f"Generated {len(refined_questions)} questions for: {document.title}" - ) - - return refined_questions - - def _generate_initial_questions(self, document: KnowledgeItem) -> List[str]: - """Generate initial set of questions based on paper content.""" - generator_llm = self._llm_blocks["question_generator"] - - # Prepare content for analysis (limit size for API efficiency) - analysis_content = self._prepare_content_for_analysis(document) - - prompt = self._render_prompt( - "generate_questions_template", - title=document.title, - abstract=document.abstract or "", - content=analysis_content, - max_questions=self.config.max_questions, - depth=self.config.question_depth, - focus_areas=", ".join(self.config.focus_areas), - ) - - try: - # Use structured output generation for initial questions - response_format = { - "type": "json_object", - "response_schema": QUESTIONS_LIST_SCHEMA, - } - structured_response = generator_llm.generate_structured_output( - prompt, response_format=response_format - ) - - if structured_response and isinstance(structured_response, dict): - questions = structured_response.get("questions", []) - # Limit to max_questions and ensure all are strings ending with '?' - questions = [ - q - for q in questions - if isinstance(q, str) and q.strip().endswith("?") - ] - questions = questions[: self.config.max_questions] - logger.debug(f"Generated {len(questions)} initial questions") - return questions - else: - logger.warning( - "No structured response received for initial questions" - ) - return [] - - except Exception as e: - logger.error(f"Error generating questions: {e}") - return [] - - def _refine_questions( - self, questions: List[str], document: KnowledgeItem - ) -> List[Dict[str, str]]: - """Refine questions and add metadata using structured output.""" - refiner_llm = self._llm_blocks["question_refiner"] - refined_questions = [] - - for i, question in enumerate(questions): - try: - refine_prompt = self._render_prompt( - "refine_question_template", - question=question, - title=document.title, - abstract=document.abstract or "", - ) - - # Use structured output generation with schema - response_format = { - "type": "json_object", - "response_schema": QUESTION_SCHEMA, - } - structured_response = refiner_llm.generate_structured_output( - refine_prompt, response_format=response_format - ) - - if structured_response and isinstance( - structured_response, dict - ): - # Validate required fields and add ID - question_data = { - "id": f"q_{i + 1}", - "question": structured_response.get( - "question", question - ), - "category": structured_response.get( - "category", "general" - ), - "difficulty": structured_response.get( - "difficulty", "intermediate" - ), - } - refined_questions.append(question_data) - else: - # Fallback to original question - refined_questions.append( - { - "id": f"q_{i + 1}", - "question": question, - "category": "general", - "difficulty": "intermediate", - } - ) - - except Exception as e: - logger.warning(f"Error refining question {i + 1}: {e}") - # Fallback to original question - refined_questions.append( - { - "id": f"q_{i + 1}", - "question": question, - "category": "general", - "difficulty": "intermediate", - } - ) - - return refined_questions - - def _prepare_content_for_analysis(self, document: KnowledgeItem) -> str: - """Prepare content for analysis, limiting size for API efficiency.""" - content = document.content or "" - - # Limit content to avoid API token limits - max_chars = 8000 # Approximately 2000 tokens - if len(content) > max_chars: - # Take first portion + last portion to capture intro and conclusion - first_half = content[: max_chars // 2] - last_half = content[-max_chars // 2 :] - content = ( - first_half + "\n\n[... content truncated ...]\n\n" + last_half - ) - - return content diff --git a/examples/pipeline/quant_paper/flows/qa_flow/prompts.yaml b/examples/pipeline/quant_paper/flows/qa_flow/prompts.yaml deleted file mode 100644 index 7fa9839..0000000 --- a/examples/pipeline/quant_paper/flows/qa_flow/prompts.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Prompt templates for QAFlow -templates: - generate_questions_template: | - You are a financial research expert and educational content creator. Your task is to generate {{ max_questions }} thoughtful, engaging questions based on the following research paper. - - Paper Title: {{ title }} - - Abstract: - {{ abstract }} - - Content (partial): - {{ content }} - - Generate {{ max_questions }} questions at {{ depth }} level focusing on: {{ focus_areas }} - - Requirements: - - Questions should promote critical thinking and deeper understanding - - Focus on methodology, implications, and practical applications - - Suitable for web display and reader engagement - - Each question should end with a question mark - - Please respond with valid JSON in the following format: - { - "questions": [ - "First thoughtful question about the research?", - "Second insightful question about the methodology?", - "Third question about practical implications?" - ] - } - - Ensure the response is valid JSON without any additional text or formatting. - - refine_question_template: | - You are an educational content specialist. Refine the following question to make it more engaging and categorize it appropriately. - - Original Question: {{ question }} - Paper Title: {{ title }} - Abstract: {{ abstract }} - - Make the question clear, specific, and thought-provoking while maintaining its educational value. - - Please respond with valid JSON in the following format: - { - "question": "refined version of the question", - "category": "methodology|findings|implications|applications|theory", - "difficulty": "basic|intermediate|advanced" - } - - Ensure the response is valid JSON without any additional text or formatting. diff --git a/examples/pipeline/quant_paper/flows/summary_flow/prompts.yaml b/examples/pipeline/quant_paper/flows/summary_flow/prompts.yaml deleted file mode 100644 index f516d2c..0000000 --- a/examples/pipeline/quant_paper/flows/summary_flow/prompts.yaml +++ /dev/null @@ -1,32 +0,0 @@ -# Prompt templates for SummaryFlow with enhanced quantitative finance focus -templates: - summarize_chunk_template: | - You are an expert quantitative finance researcher and analyst. Summarize the following research content with a focus on: - - 1. Key quantitative methodologies and mathematical models - 2. Empirical findings and statistical results - 3. Practical applications in finance - 4. Novel contributions to the field - 5. Data sources and experimental design - - Maintain technical accuracy while ensuring clarity. Include specific metrics, formulas, or results when mentioned. - - Content: - {{ chunk_text }} - - Comprehensive Summary: - - combine_summaries_template: | - You are an expert quantitative finance researcher. Synthesize the following chunk summaries into a coherent, comprehensive final summary. - - Requirements: - - Create a well-structured overview that flows logically - - Preserve technical details and quantitative insights - - Eliminate redundancy while maintaining completeness - - Highlight the most significant contributions and findings - - Organize into clear sections (methodology, findings, implications) - - Chunk Summaries: - {{ summaries }} - - Final Comprehensive Summary: diff --git a/examples/pipeline/quant_paper/pipeline.py b/examples/pipeline/quant_paper/pipeline.py deleted file mode 100644 index 4bc483c..0000000 --- a/examples/pipeline/quant_paper/pipeline.py +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env python3 -"""Production-level Quant Paper Agent Pipeline using Gemini 2.5 Pro & Flash. - -This pipeline demonstrates: -1. Real-world paper extraction from ArXiv -2. Advanced PDF parsing with LlamaParser -3. Intelligent summarization using Gemini models -4. QA generation for web engagement -5. Storage with rich metadata using meta_info approach -""" - -import sys -from pathlib import Path -from typing import Optional - -from flows.qa_flow.flow import QAFlow - -from quantmind.config.settings import load_config -from quantmind.flow.summary_flow import SummaryFlow -from quantmind.models.paper import Paper -from quantmind.parsers.llama_parser import LlamaParser -from quantmind.sources.arxiv_source import ArxivSource -from quantmind.storage.local_storage import LocalStorage -from quantmind.utils.logger import get_logger - -logger = get_logger(__name__) - - -# Color codes for better terminal output -class Colors: - """ANSI color codes for terminal output.""" - - HEADER = "\033[95m" - OKBLUE = "\033[94m" - OKCYAN = "\033[96m" - OKGREEN = "\033[92m" - WARNING = "\033[93m" - FAIL = "\033[91m" - ENDC = "\033[0m" - BOLD = "\033[1m" - UNDERLINE = "\033[4m" - - # Emoji-like symbols - ROCKET = "🚀" - CHECK = "✓" - CROSS = "❌" - ARROW = "📡" - DOWNLOAD = "📥" - SEARCH = "🔍" - WRITE = "📝" - QUESTION = "❓" - SAVE = "💾" - CELEBRATE = "🎉" - INFO = "📄" - GEAR = "🔧" - STOP = "⏹️" - BULB = "💡" - - -def cprint(text: str, color: str = Colors.ENDC, bold: bool = False) -> None: - """Print colored text to terminal.""" - prefix = Colors.BOLD if bold else "" - print(f"{prefix}{color}{text}{Colors.ENDC}") - - -def print_header(text: str) -> None: - """Print a styled header.""" - cprint(f"\n{Colors.ROCKET} {text}", Colors.HEADER, bold=True) - - -def print_success(text: str) -> None: - """Print a success message.""" - cprint(f"{Colors.CHECK} {text}", Colors.OKGREEN) - - -def print_error(text: str) -> None: - """Print an error message.""" - cprint(f"{Colors.CROSS} {text}", Colors.FAIL, bold=True) - - -def print_info(text: str) -> None: - """Print an info message.""" - cprint(f"{Colors.INFO} {text}", Colors.OKBLUE) - - -def print_step(icon: str, text: str) -> None: - """Print a pipeline step.""" - cprint(f"\n{icon} {text}", Colors.OKCYAN, bold=True) - - -def get_unique_paper( - arxiv_source: ArxivSource, local_storage: LocalStorage, retry_count: int = 3 -) -> Optional[Paper]: - """Get a unique paper from ArXiv focusing on quantitative finance.""" - search_queries = [ - "Large Language Models quantitative finance", - "machine learning portfolio optimization", - "deep learning algorithmic trading", - "neural networks risk management finance", - "AI financial markets", - ] - - for query in search_queries: - logger.info(f"Searching with query: {query}") - - for attempt in range(retry_count): - try: - papers = arxiv_source.search( - query=query, - max_results=3, - ) - - if not papers: - logger.warning(f"No papers found for query: {query}") - continue - - for paper in papers: - paper_id = paper.get_primary_id() - if paper_id not in local_storage._raw_files_index: - logger.info(f"Found unique paper: {paper.title}") - return paper - else: - logger.debug(f"Paper {paper_id} already processed") - - except Exception as e: - logger.error(f"Error searching (attempt {attempt + 1}): {e}") - if attempt == retry_count - 1: - logger.error( - f"Failed to search after {retry_count} attempts" - ) - - logger.warning("No unique papers found across all queries") - return None - - -def display_results(paper: Paper) -> None: - """Display pipeline results in a colorful, user-friendly format.""" - # Header - cprint("\n" + "=" * 80, Colors.HEADER) - cprint("🎯 QUANT PAPER AGENT PIPELINE RESULTS", Colors.HEADER, bold=True) - cprint("=" * 80, Colors.HEADER) - - # Paper Information - print_info("PAPER INFORMATION:") - cprint(f"Title: {paper.title}", Colors.OKBLUE, bold=True) - cprint(f"Authors: {', '.join(paper.authors)}", Colors.OKBLUE) - cprint(f"ArXiv ID: {paper.arxiv_id}", Colors.OKBLUE) - cprint(f"Categories: {', '.join(paper.categories)}", Colors.OKBLUE) - cprint(f"Published: {paper.published_date}", Colors.OKBLUE) - - # Display summary - summary = paper.meta_info.get("summary") - if summary: - print_step("📋", "INTELLIGENT SUMMARY:") - cprint("-" * 40, Colors.OKCYAN) - # Truncate summary for display if too long - display_summary = ( - summary[:500] + "..." if len(summary) > 500 else summary - ) - cprint(display_summary, Colors.ENDC) - - # Display QA questions - qa_data = paper.meta_info.get("qa_data") - if qa_data: - questions = qa_data.get("questions", []) - print_step("❓", f"THOUGHTFUL QUESTIONS ({len(questions)} generated):") - cprint("-" * 40, Colors.OKCYAN) - - for i, q_data in enumerate(questions, 1): - question = q_data.get("question", "") - category = q_data.get("category", "general") - difficulty = q_data.get("difficulty", "intermediate") - - # Color code by difficulty - difficulty_color = { - "basic": Colors.OKGREEN, - "intermediate": Colors.WARNING, - "advanced": Colors.FAIL, - }.get(difficulty.lower(), Colors.ENDC) - - cprint(f"\n{i}. {question}", Colors.ENDC, bold=True) - print( - f" {Colors.ENDC}Category: {Colors.OKCYAN}{category.title()}{Colors.ENDC} | Difficulty: {difficulty_color}{difficulty.title()}{Colors.ENDC}" - ) - - # Display metadata - print_step("🔧", "PROCESSING METADATA:") - cprint("-" * 40, Colors.OKCYAN) - if qa_data: - cprint(f"QA Model: {qa_data.get('model_used', 'Unknown')}", Colors.ENDC) - cprint( - f"Generated: {qa_data.get('generated_at', 'Unknown')}", Colors.ENDC - ) - cprint( - f"Question Count: {qa_data.get('question_count', 0)}", Colors.ENDC - ) - cprint( - f"Focus Areas: {', '.join(qa_data.get('focus_areas', []))}", - Colors.ENDC, - ) - - storage_path = paper.meta_info.get("storage_path", "Not stored") - cprint(f"\nStorage Path: {storage_path}", Colors.OKGREEN) - cprint("=" * 80, Colors.HEADER) - - -def main(): - """Execute the production quant paper agent pipeline.""" - print_header("Starting Production Quant Paper Agent Pipeline") - cprint( - "Using Gemini 2.5 Pro & Flash for advanced AI processing\n", - Colors.OKCYAN, - ) - - try: - # Step 1: Load configuration - config_path = Path(__file__).parent / "config.yaml" - if not config_path.exists(): - raise FileNotFoundError( - f"Configuration file not found: {config_path}" - ) - - logger.info("Loading configuration...") - settings = load_config(config_path) - - # Step 2: Initialize components - logger.info("Initializing pipeline components...") - local_storage = LocalStorage(settings.storage) - arxiv_source = ArxivSource(settings.source) - llama_parser = LlamaParser(settings.parser) - - # Initialize flows - summary_flow = SummaryFlow(settings.flows["summary_flow"]) - qa_flow = QAFlow(settings.flows["qa_flow"]) - - print_success("All components initialized successfully") - - # Step 3: Get unique paper - print_step("📡", "Searching for unique quantitative finance paper...") - paper = get_unique_paper(arxiv_source, local_storage) - if not paper: - print_error( - "No unique papers found. Try again later or check your search criteria." - ) - return - - print_success(f"Found paper: {paper.title[:60]}...") - - # Step 4: Download and store raw content - print_step("📥", "Downloading paper content...") - local_storage.process_knowledge(paper) - print_success("Paper downloaded successfully") - - # Step 5: Parse with LlamaParser - print_step("🔍", "Parsing PDF content with LlamaParser...") - paper = llama_parser.parse_paper(paper) - - if not paper.has_content(): - print_error("Failed to extract content from PDF") - return - - print_success(f"Content extracted ({len(paper.content)} characters)") - - # Step 6: Generate summary using Gemini 2.5 - print_step("📝", "Generating intelligent summary with Gemini 2.5...") - summary_result = summary_flow.run(paper) - - # Store summary in meta_info - paper.meta_info["summary"] = summary_result - paper.meta_info["summary_generated_at"] = paper.processed_at.isoformat() - - print_success("Summary generated successfully") - - # Step 7: Generate QA questions - print_step("❓", "Generating thoughtful questions with Gemini 2.5...") - qa_result = qa_flow.run(paper) - # QA results are automatically stored in paper.meta_info by the flow - - print_success(f"Generated {len(qa_result)} thoughtful questions") - - # Step 8: Store final enriched paper - print_step("💾", "Storing enriched paper with metadata...") - local_storage.store_knowledge(paper) - - # Add storage path to meta_info - storage_path = local_storage.get_knowledge_path(paper.get_primary_id()) - paper.meta_info["storage_path"] = str(storage_path) - - print_success("Paper stored with all metadata") - - # Step 9: Display results - display_results(paper) - - cprint( - f"\n🎉 Pipeline completed successfully!", Colors.OKGREEN, bold=True - ) - cprint(f"Paper stored at: {storage_path}", Colors.OKGREEN) - - except KeyboardInterrupt: - cprint("\n⏹️ Pipeline interrupted by user", Colors.WARNING, bold=True) - except Exception as e: - logger.error(f"Pipeline failed: {e}") - print_error(f"Pipeline failed: {e}") - cprint("💡 Make sure you have:", Colors.OKCYAN, bold=True) - cprint(" • Set GOOGLE_API_KEY environment variable", Colors.OKCYAN) - cprint( - " • Set LLAMA_CLOUD_API_KEY environment variable", Colors.OKCYAN - ) - cprint(" • Valid internet connection", Colors.OKCYAN) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/examples/sources/README.md b/examples/sources/README.md deleted file mode 100644 index ae74e94..0000000 --- a/examples/sources/README.md +++ /dev/null @@ -1,212 +0,0 @@ -# Sources Usage Examples - -This directory contains comprehensive examples demonstrating how to use the QuantMind sources module, specifically the ArXiv source implementation. - -## Files Overview - -### 1. `basic_arxiv_usage.py` -Demonstrates fundamental ArXiv source operations: -- Basic paper search -- Retrieving papers by ArXiv ID -- Getting recent papers by timeframe -- Batch retrieval of multiple papers -- PDF download functionality -- Category-specific searches - -**Run with:** -```bash -python examples/sources/basic_arxiv_usage.py -``` - -### 2. `advanced_configuration.py` -Shows advanced configuration options and scenarios: -- Finance-focused research configuration -- AI/ML research configuration -- Production-ready settings -- Loading configuration from YAML -- Configuration validation -- Comparing different configuration approaches - -**Run with:** -```bash -python examples/sources/advanced_configuration.py -``` - -## Key Features Demonstrated - -### Configuration Management -- **Pydantic-based validation**: All configurations use structured Pydantic models -- **Flexible initialization**: Accept both dict and config objects -- **Environment-specific settings**: Examples for development, research, and production -- **YAML support**: Load configurations from external files - -### Content Filtering -- **Category filtering**: Include/exclude specific arXiv categories -- **Quality controls**: Minimum abstract length requirements -- **Content validation**: Ensure papers meet quality standards - -### Download Management -- **Configurable downloads**: Enable/disable PDF downloads -- **Custom directories**: Specify download locations -- **Batch downloads**: Download multiple papers efficiently -- **Error handling**: Robust error handling for failed downloads - -### Rate Limiting -- **Respectful usage**: Built-in rate limiting to respect arXiv's servers -- **Configurable rates**: Adjust request frequency based on use case -- **Timeout handling**: Configurable timeouts for reliability - -## Configuration Examples - -### Basic Configuration -```python -from quantmind.config.sources import ArxivSourceConfig -from quantmind.sources.arxiv_source import ArxivSource - -# Simple configuration -config = ArxivSourceConfig( - max_results=10, - download_pdfs=True, - download_dir="./papers" -) - -source = ArxivSource(config=config) -``` - -### Finance Research Configuration -```python -config = ArxivSourceConfig( - include_categories=["q-fin.ST", "q-fin.TR", "q-fin.PM"], - min_abstract_length=150, - requests_per_second=0.5, - sort_by="submittedDate" -) -``` - -### Production Configuration -```python -config = ArxivSourceConfig( - max_results=100, - timeout=60, - retry_attempts=3, - requests_per_second=0.5, - min_abstract_length=200, - download_pdfs=True -) -``` - -## Usage Patterns - -### 1. Basic Search -```python -source = ArxivSource() -papers = source.search("machine learning", max_results=5) -``` - -### 2. Timeframe Queries -```python -# Get papers from last 7 days in AI categories -papers = source.get_by_timeframe( - days=7, - categories=["cs.AI", "cs.LG"] -) -``` - -### 3. Batch Retrieval -```python -paper_ids = ["1706.03762", "1512.03385"] -papers = source.get_batch(paper_ids) -``` - -### 4. PDF Downloads -```python -config = ArxivSourceConfig(download_pdfs=True, download_dir="./pdfs") -source = ArxivSource(config=config) -papers = source.search("neural networks", max_results=3) -paths = source.download_papers_pdfs(papers) -``` - -## Best Practices - -### 1. Rate Limiting -Always use appropriate rate limiting to be respectful to arXiv: -```python -config = ArxivSourceConfig(requests_per_second=1.0) # 1 request per second -``` - -### 2. Content Quality -Filter for high-quality content: -```python -config = ArxivSourceConfig( - min_abstract_length=100, # Ensure substantial abstracts - include_categories=["relevant", "categories"] # Focus on relevant areas -) -``` - -### 3. Error Handling -Always handle potential errors: -```python -try: - papers = source.search(query) - if not papers: - print("No papers found") -except Exception as e: - logger.error(f"Search failed: {e}") -``` - -### 4. Configuration Validation -Validate configurations before use: -```python -source = ArxivSource(config=config) -if source.validate_config(): - # Proceed with operations - pass -else: - # Handle invalid configuration - pass -``` - -## Advanced Features - -### 1. Custom Filtering -Implement additional filtering logic: -```python -papers = source.search(query, max_results=100) -filtered_papers = [p for p in papers if custom_filter(p)] -``` - -### 3. Batch Processing -Process papers in batches for efficiency: -```python -def process_batch(papers): - # Process batch of papers - paths = source.download_papers_pdfs(papers) - return paths - -papers = source.search(query, max_results=50) -batch_size = 10 -for i in range(0, len(papers), batch_size): - batch = papers[i:i+batch_size] - process_batch(batch) -``` - -## Notes - -- **ArXiv Compliance**: All examples follow arXiv's API usage guidelines -- **Error Handling**: Comprehensive error handling for network issues -- **Logging**: Built-in logging for debugging and monitoring -- **Testing**: Examples include test scenarios for validation -- **Performance**: Optimized for both small queries and large-scale research - -## Requirements - -To run these examples, ensure you have: -- `arxiv` Python package for API access -- `requests` for PDF downloads -- `pydantic` for configuration validation -- `pyyaml` for YAML configuration support - -Install requirements: -```bash -pip install arxiv requests pydantic pyyaml -``` diff --git a/examples/sources/advanced_configuration.py b/examples/sources/advanced_configuration.py deleted file mode 100644 index 160b545..0000000 --- a/examples/sources/advanced_configuration.py +++ /dev/null @@ -1,294 +0,0 @@ -"""Advanced ArXiv source configuration examples. - -This example demonstrates advanced configuration options for the ArxivSource: -- Custom download directories -- Category filtering -- Rate limiting -- Content filtering -- Configuration validation -""" - -import yaml -from pathlib import Path -from tempfile import TemporaryDirectory - -from quantmind.config.sources import ArxivSourceConfig -from quantmind.sources.arxiv_source import ArxivSource -from quantmind.utils.logger import get_logger - -logger = get_logger(__name__) - - -def finance_focused_config_example(): - """Demonstrate finance-focused configuration.""" - print("=== Finance-Focused Configuration ===") - - config = ArxivSourceConfig( - # API settings - max_results=20, - sort_by="submittedDate", - sort_order="descending", - # Content filtering for finance - include_categories=[ - "q-fin.ST", # Statistical Finance - "q-fin.TR", # Trading and Market Microstructure - "q-fin.PM", # Portfolio Management - "q-fin.RM", # Risk Management - "q-fin.CP", # Computational Finance - ], - min_abstract_length=150, # Longer abstracts for quality - # Rate limiting to be respectful - requests_per_second=0.8, - timeout=45, - # Download settings - download_pdfs=True, - ) - - print("Configuration created:") - print(f"- Max results: {config.max_results}") - print(f"- Include categories: {config.include_categories}") - print(f"- Min abstract length: {config.min_abstract_length}") - print(f"- Requests per second: {config.requests_per_second}") - print(f"- Download PDFs: {config.download_pdfs}") - - return config - - -def ai_research_config_example(): - """Demonstrate AI research configuration.""" - print("\n=== AI Research Configuration ===") - - config = ArxivSourceConfig( - # API settings optimized for AI research - max_results=50, - sort_by="relevance", - sort_order="descending", - # AI/ML categories - include_categories=[ - "cs.AI", # Artificial Intelligence - "cs.LG", # Machine Learning - "cs.CV", # Computer Vision - "cs.CL", # Computation and Language - "cs.NE", # Neural and Evolutionary Computing - "stat.ML", # Machine Learning (Statistics) - ], - # Exclude some categories we're not interested in - exclude_categories=[ - "cs.CR", # Cryptography and Security - "cs.SE", # Software Engineering - ], - # Quality filters - min_abstract_length=100, - # Higher rate for research use - requests_per_second=1.5, - # No downloads for this config - download_pdfs=False, - ) - - print("AI Research configuration:") - print(f"- Focus areas: {len(config.include_categories)} categories") - print(f"- Excluded: {config.exclude_categories}") - print(f"- Sort by: {config.sort_by}") - - return config - - -def production_config_example(): - """Demonstrate production-ready configuration.""" - print("\n=== Production Configuration ===") - - with TemporaryDirectory() as temp_dir: - download_dir = Path(temp_dir) / "arxiv_papers" - - config = ArxivSourceConfig( - # Conservative settings for production - max_results=100, - timeout=60, - retry_attempts=3, - requests_per_second=0.5, # Very conservative - # Content quality controls - min_abstract_length=200, - # Download setup - download_pdfs=True, - download_dir=download_dir, - # Broad categories for general research - include_categories=[ - "cs.AI", - "cs.LG", - "stat.ML", # AI/ML - "q-fin.ST", - "q-fin.TR", - "q-fin.PM", # Finance - "math.OC", - "math.PR", - "math.ST", # Math - ], - ) - - print("Production configuration:") - print(f"- Download directory: {config.download_dir}") - print(f"- Timeout: {config.timeout}s") - print(f"- Retry attempts: {config.retry_attempts}") - print(f"- Rate limit: {config.requests_per_second} req/s") - - return config - - -def config_from_yaml_example(): - """Demonstrate loading configuration from YAML.""" - print("\n=== Configuration from YAML ===") - - yaml_config = """ - max_results: 25 - sort_by: "submittedDate" - sort_order: "descending" - - # Download settings - download_pdfs: true - - # Quality filters - min_abstract_length: 120 - - # Categories of interest - include_categories: - - "q-fin.ST" - - "q-fin.TR" - - "cs.AI" - - "stat.ML" - - # Rate limiting - requests_per_second: 1.0 - timeout: 30 - """ - - # Parse YAML - yaml_data = yaml.safe_load(yaml_config) - - # Create config from dictionary - config = ArxivSourceConfig(**yaml_data) - - print("Configuration loaded from YAML:") - print(f"- Max results: {config.max_results}") - print(f"- Categories: {len(config.include_categories)}") - print(f"- Download PDFs: {config.download_pdfs}") - - return config - - -def test_configurations(): - """Test different configurations with actual searches.""" - print("\n=== Testing Configurations ===") - - # Test finance config - finance_config = finance_focused_config_example() - finance_source = ArxivSource(config=finance_config) - - print("\nTesting finance configuration:") - if finance_source.validate_config(): - papers = finance_source.search("portfolio optimization", max_results=3) - print(f"✓ Found {len(papers)} finance papers") - for paper in papers: - print(f" - {paper.title[:50]}...") - else: - print("✗ Finance configuration invalid") - - # Test AI config - ai_config = ai_research_config_example() - ai_source = ArxivSource(config=ai_config) - - print("\nTesting AI configuration:") - if ai_source.validate_config(): - papers = ai_source.search("transformer neural network", max_results=3) - print(f"✓ Found {len(papers)} AI papers") - for paper in papers: - print(f" - {paper.title[:50]}...") - else: - print("✗ AI configuration invalid") - - -def config_validation_example(): - """Demonstrate configuration validation.""" - print("\n=== Configuration Validation ===") - - # Valid configuration - try: - valid_config = ArxivSourceConfig( - max_results=10, sort_by="relevance", requests_per_second=1.0 - ) - print("✓ Valid configuration created successfully") - except Exception as e: - print(f"✗ Valid configuration failed: {e}") - - # Invalid configurations - invalid_configs = [ - {"sort_by": "invalid_sort"}, - {"sort_order": "invalid_order"}, - {"max_results": -1}, - {"requests_per_second": 0}, - ] - - for i, invalid_config in enumerate(invalid_configs, 1): - try: - ArxivSourceConfig(**invalid_config) - print(f"✗ Invalid config {i} should have failed!") - except Exception as e: - print( - f"✓ Invalid config {i} correctly rejected: {type(e).__name__}" - ) - - -def compare_configurations(): - """Compare search results across different configurations.""" - print("\n=== Configuration Comparison ===") - - query = "machine learning finance" - - configs = { - "Default": ArxivSourceConfig(), - "Relevance": ArxivSourceConfig(sort_by="relevance"), - "Recent": ArxivSourceConfig(sort_by="submittedDate"), - "Finance-only": ArxivSourceConfig( - include_categories=["q-fin.ST", "q-fin.TR"] - ), - } - - for name, config in configs.items(): - source = ArxivSource(config=config) - papers = source.search(query, max_results=3) - - print(f"\n{name} configuration:") - print(f" Found {len(papers)} papers") - if papers: - print(f" Top result: {papers[0].title[:60]}...") - print(f" Categories: {papers[0].categories}") - - -def main(): - """Run all configuration examples.""" - examples = [ - config_validation_example, - config_from_yaml_example, - test_configurations, - compare_configurations, - ] - - for i, example in enumerate(examples, 1): - try: - print(f"\n{'=' * 70}") - print(f"Configuration Example {i}/{len(examples)}") - example() - except Exception as e: - logger.error(f"Error in configuration example {i}: {e}") - - # Small delay between examples - if i < len(examples): - import time - - time.sleep(0.5) - - print(f"\n{'=' * 70}") - print("All configuration examples completed!") - - -if __name__ == "__main__": - main() diff --git a/examples/sources/basic_arxiv_usage.py b/examples/sources/basic_arxiv_usage.py deleted file mode 100644 index cd5878f..0000000 --- a/examples/sources/basic_arxiv_usage.py +++ /dev/null @@ -1,220 +0,0 @@ -"""Basic ArXiv source usage example. - -This example demonstrates how to use the ArxivSource class for: -- Basic paper search -- Retrieving papers by ID -- Getting recent papers -- Configuring the source with different settings -""" - -from pathlib import Path -from tempfile import TemporaryDirectory - -from quantmind.config.sources import ArxivSourceConfig -from quantmind.sources.arxiv_source import ArxivSource -from quantmind.utils.logger import get_logger - -logger = get_logger(__name__) - - -def basic_search_example(): - """Demonstrate basic search functionality.""" - print("=== Basic Search Example ===") - - # Create source with default configuration - source = ArxivSource() - - # Search for machine learning papers - papers = source.search("machine learning", max_results=5) - - print(f"Found {len(papers)} papers:") - for i, paper in enumerate(papers, 1): - print(f"{i}. {paper.title}") - print( - f" Authors: {', '.join(paper.authors[:3])}{'...' if len(paper.authors) > 3 else ''}" - ) - print(f" Categories: {', '.join(paper.categories)}") - print(f" ArXiv ID: {paper.arxiv_id}") - print() - - -def configured_search_example(): - """Demonstrate search with custom configuration.""" - print("=== Configured Search Example ===") - - # Create configuration for finance-focused search - config = ArxivSourceConfig( - max_results=10, - sort_by="relevance", - sort_order="descending", - include_categories=["q-fin.ST", "q-fin.TR", "q-fin.PM"], - min_abstract_length=100, - requests_per_second=0.5, # Slower to be respectful - ) - - source = ArxivSource(config=config) - - # Search for quantitative finance papers - papers = source.search("portfolio optimization", max_results=3) - - print(f"Found {len(papers)} finance papers:") - for paper in papers: - print(f"- {paper.title}") - print(f" Categories: {', '.join(paper.categories)}") - print(f" Abstract length: {len(paper.abstract)} chars") - print() - - -def get_by_id_example(): - """Demonstrate retrieving papers by ID.""" - print("=== Get by ID Example ===") - - source = ArxivSource() - - # Try to get a specific paper by arXiv ID - paper_ids = [ - "2301.12345", - "1706.03762", - ] # Second one is "Attention Is All You Need" - - for paper_id in paper_ids: - paper = source.get_by_id(paper_id) - if paper: - print(f"Found paper: {paper.title}") - print(f"Authors: {', '.join(paper.authors)}") - print(f"Published: {paper.published_date}") - print() - else: - print(f"Paper {paper_id} not found") - - -def recent_papers_example(): - """Demonstrate getting recent papers.""" - print("=== Recent Papers Example ===") - - source = ArxivSource() - - # Get recent AI papers from the last 3 days - papers = source.get_by_timeframe(days=3, categories=["cs.AI", "cs.LG"]) - - print(f"Found {len(papers)} recent AI/ML papers:") - for paper in papers[:5]: # Show first 5 - print(f"- {paper.title}") - print(f" Published: {paper.published_date}") - print() - - -def download_example(): - """Demonstrate PDF download functionality.""" - print("=== PDF Download Example ===") - - with TemporaryDirectory() as temp_dir: - # Configure source with PDF downloads enabled - config = ArxivSourceConfig( - download_pdfs=True, - download_dir=Path(temp_dir), - max_results=2, - requests_per_second=0.5, - proxies={ - "http": "http://127.0.0.1:7890", - "https": "http://127.0.0.1:7890", - "all_proxy": "socks5://127.0.0.1:7890", - }, - ) - - source = ArxivSource(config=config) - - # Search for a few papers - papers = source.search("attention mechanism", max_results=2) - - if papers: - print(f"Downloading PDFs for {len(papers)} papers...") - - # Download PDFs - download_paths = source.download_papers_pdfs(papers) - - for paper, path in zip(papers, download_paths): - if path: - print(f"✓ Downloaded: {paper.title}") - print(f" File: {path.name}") - print(f" Size: {path.stat().st_size} bytes") - else: - print(f"✗ Failed to download: {paper.title}") - print() - else: - print("No papers found for download example") - - -def batch_retrieval_example(): - """Demonstrate batch retrieval of papers.""" - print("=== Batch Retrieval Example ===") - - source = ArxivSource() - - # List of paper IDs to retrieve - paper_ids = [ - "1706.03762", # Attention Is All You Need - "1512.03385", # ResNet - "1409.1556", # GAN - "nonexistent", # This one won't be found - ] - - papers = source.get_batch(paper_ids) - - print(f"Requested {len(paper_ids)} papers, found {len(papers)}:") - for paper in papers: - print(f"- {paper.title}") - print(f" ArXiv ID: {paper.arxiv_id}") - print() - - -def category_search_example(): - """Demonstrate category-specific search.""" - print("=== Category Search Example ===") - - source = ArxivSource() - - # Search in specific categories - categories = ["q-fin.ST", "cs.AI"] - - for category in categories: - papers = source.search_by_category(category, max_results=3) - print(f"Category {category}: {len(papers)} papers") - - for paper in papers: - print(f" - {paper.title[:60]}...") - print() - - -def main(): - """Run all examples.""" - examples = [ - basic_search_example, - configured_search_example, - get_by_id_example, - recent_papers_example, - batch_retrieval_example, - category_search_example, - download_example, # This one creates files, so run it last - ] - - for i, example in enumerate(examples, 1): - try: - print(f"\n{'=' * 60}") - print(f"Example {i}/{len(examples)}") - example() - except Exception as e: - logger.error(f"Error in example {i}: {e}") - - # Add a small delay between examples to be respectful to arXiv - if i < len(examples): - import time - - time.sleep(1) - - print(f"\n{'=' * 60}") - print("All examples completed!") - - -if __name__ == "__main__": - main() diff --git a/examples/storage/local_storage_usage.py b/examples/storage/local_storage_usage.py deleted file mode 100644 index 245ce46..0000000 --- a/examples/storage/local_storage_usage.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Local storage usage example for QuantMind. - -This example demonstrates: -1. Storing raw files from content bytes directly -2. Using process_knowledge for Paper objects -3. Automatic PDF downloading for Paper objects -""" - -from datetime import datetime, timezone -from pathlib import Path - -from quantmind.config import LocalStorageConfig -from quantmind.models import Paper -from quantmind.storage import LocalStorage - - -def demonstrate_enhanced_raw_file_storage(): - """Demonstrate storing raw files from content.""" - print("=== Enhanced Raw File Storage Demo ===") - - # Initialize storage - config = LocalStorageConfig(storage_dir=Path("./demo_data")) - storage = LocalStorage(config) - - # Example 1: Store content directly as bytes - print("\n1. Storing content directly as bytes:") - - # Simulate PDF content downloaded from ArXiv - pdf_content = b"%PDF-1.4\n1 0 obj<>endobj" - - # Store PDF content directly - pdf_path = storage.store_raw_file( - file_id="paper_1", content=pdf_content, file_extension=".pdf" - ) - print(f" Stored PDF content to: {pdf_path}") - - # Store text content - text_content = "# Research Paper Abstract\n\nSample content".encode("utf-8") - - text_path = storage.store_raw_file( - file_id="paper_1_abstract", content=text_content, file_extension=".md" - ) - print(f" Stored text content to: {text_path}") - - return storage - - -def demonstrate_paper_specialized_storage(): - """Demonstrate Paper-specific storage with automatic handling.""" - print("\n=== Paper Specialized Storage Demo ===") - - # Initialize storage - config = LocalStorageConfig(storage_dir=Path("./demo_data")) - storage = LocalStorage(config) - - # Paper with PDF URL - paper_with_pdf = Paper( - title="Machine Learning in Quantitative Finance", - abstract="This paper explores ML techniques in quantitative finance.", - authors=["John Smith", "Jane Doe"], - arxiv_id="2024.0001", - pdf_url="https://arxiv.org/pdf/2024.0001.pdf", - categories=["q-fin.CP", "cs.LG"], - published_date=datetime.now(timezone.utc), - source="arxiv", - ) - - # Store with specialized handling - paper_id = storage.process_knowledge(paper_with_pdf) - print(f" Stored paper with ID: {paper_id}") - - return storage - - -def main(): - """Main demonstration function.""" - print("🚀 QuantMind Enhanced Storage Demonstration") - - try: - demonstrate_enhanced_raw_file_storage() - demonstrate_paper_specialized_storage() - print(f"\n🎉 All demonstrations completed successfully!") - - except Exception as e: - print(f"\n❌ Error during demonstration: {e}") - raise - - -if __name__ == "__main__": - main() diff --git a/examples/storage/storage_performance_demo.py b/examples/storage/storage_performance_demo.py deleted file mode 100644 index 8eccfd0..0000000 --- a/examples/storage/storage_performance_demo.py +++ /dev/null @@ -1,244 +0,0 @@ -"""Storage performance demonstration with and without indexing. - -This script demonstrates the dramatic performance improvement achieved -by the new indexing system in LocalStorage. -""" - -import time -from pathlib import Path -from datetime import datetime, timezone - -from quantmind.config.storage import LocalStorageConfig -from quantmind.storage.local_storage import LocalStorage -from quantmind.models.paper import Paper - - -def create_test_data(storage: LocalStorage, num_items: int = 100): - """Create test data for performance testing.""" - print(f"Creating {num_items} test items...") - - for i in range(num_items): - # Create test papers - paper = Paper( - title=f"Test Paper {i}", - abstract=f"This is test paper number {i} for performance testing.", - authors=[f"Author {i}"], - arxiv_id=f"test.{i:04d}", - categories=["q-fin.CP"], - published_date=datetime.now(timezone.utc), - source="test", - ) - storage.store_knowledge(paper) - - # Create test raw files - content = f"Test content for file {i}".encode() - storage.store_raw_file( - f"test_file_{i}", content=content, file_extension=".txt" - ) - - # Create test embeddings - embedding = [float(j) for j in range(10)] # Simple embedding - storage.store_embedding(f"test.{i:04d}", embedding, "test_model") - - print(f"✅ Created {num_items} items successfully") - - -def simulate_old_behavior(storage: LocalStorage, num_lookups: int = 50): - """Simulate old file system scanning behavior for comparison.""" - print(f"\n🐌 Simulating old behavior (directory scanning)...") - - start_time = time.time() - - for i in range(num_lookups): - # Simulate directory scanning by manually checking files - file_id = f"test_file_{i}" - found = False - - # This simulates the old glob-based search - for file_path in storage.config.raw_files_dir.glob(f"{file_id}.*"): - if file_path.is_file(): - found = True - break - - end_time = time.time() - old_time = end_time - start_time - - print(f" Time for {num_lookups} lookups: {old_time:.4f} seconds") - print(f" Average per lookup: {(old_time / num_lookups) * 1000:.2f} ms") - - return old_time - - -def test_new_indexing_performance(storage: LocalStorage, num_lookups: int = 50): - """Test the new indexing system performance.""" - print(f"\n🚀 Testing new indexing system...") - - start_time = time.time() - - for i in range(num_lookups): - # Use the new index-based lookup - file_id = f"test_file_{i}" - file_path = storage.get_raw_file(file_id) - # Just verify it exists - assert file_path is not None - - end_time = time.time() - new_time = end_time - start_time - - print(f" Time for {num_lookups} lookups: {new_time:.4f} seconds") - print(f" Average per lookup: {(new_time / num_lookups) * 1000:.2f} ms") - - return new_time - - -def test_knowledge_lookup_performance( - storage: LocalStorage, num_lookups: int = 50 -): - """Test knowledge lookup performance.""" - print(f"\n📚 Testing knowledge lookup performance...") - - start_time = time.time() - - for i in range(num_lookups): - knowledge_id = f"test.{i:04d}" - knowledge = storage.get_knowledge(knowledge_id) - assert knowledge is not None - - end_time = time.time() - lookup_time = end_time - start_time - - print( - f" Time for {num_lookups} knowledge lookups: {lookup_time:.4f} seconds" - ) - print(f" Average per lookup: {(lookup_time / num_lookups) * 1000:.2f} ms") - - return lookup_time - - -def test_batch_operations(storage: LocalStorage): - """Test batch operations performance.""" - print(f"\n📦 Testing batch operations...") - - # Test get_all_knowledges (now uses index) - start_time = time.time() - all_knowledges = list(storage.get_all_knowledges()) - end_time = time.time() - - batch_time = end_time - start_time - count = len(all_knowledges) - - print(f" Retrieved {count} knowledge items in {batch_time:.4f} seconds") - print(f" Average per item: {(batch_time / count) * 1000:.2f} ms") - - return batch_time - - -def test_index_rebuild_performance(storage: LocalStorage): - """Test index rebuilding performance.""" - print(f"\n🔄 Testing index rebuild performance...") - - start_time = time.time() - storage.rebuild_all_indexes() - end_time = time.time() - - rebuild_time = end_time - start_time - - print(f" Index rebuild time: {rebuild_time:.4f} seconds") - print(f" Raw files indexed: {len(storage._raw_files_index)}") - print(f" Knowledge items indexed: {len(storage._knowledges_index)}") - print(f" Embeddings indexed: {len(storage._embeddings_index)}") - - return rebuild_time - - -def show_storage_statistics(storage: LocalStorage): - """Show detailed storage statistics.""" - print(f"\n📊 Storage Statistics:") - - info = storage.get_storage_info() - - print(f" Storage Directory: {info['storage_dir']}") - print(f" Knowledge Count: {info['knowledge_count']}") - print(f" Raw Files Count: {info['raw_files_count']}") - print(f" Embeddings Count: {info['embeddings_count']}") - - print(f"\n Index Statistics:") - for index_type, stats in info["indexes"].items(): - print(f" {index_type}: {stats['entries']} entries") - print(f" Index file: {Path(stats['index_file']).name}") - - -def main(): - """Main performance demonstration.""" - print("🎯 QuantMind Storage Performance Demonstration") - print("=" * 60) - - # Setup - demo_dir = Path("./performance_demo_data") - if demo_dir.exists(): - import shutil - - shutil.rmtree(demo_dir) - - config = LocalStorageConfig(storage_dir=demo_dir) - storage = LocalStorage(config) - - try: - # Create test data - num_items = 100 - num_lookups = 50 - - create_test_data(storage, num_items) - - # Performance tests - old_time = simulate_old_behavior(storage, num_lookups) - new_time = test_new_indexing_performance(storage, num_lookups) - knowledge_time = test_knowledge_lookup_performance(storage, num_lookups) - batch_time = test_batch_operations(storage) - rebuild_time = test_index_rebuild_performance(storage) - - # Calculate improvement - if old_time > 0: - speedup = old_time / new_time - improvement = ((old_time - new_time) / old_time) * 100 - else: - speedup = float("inf") - improvement = 100 - - # Summary - print(f"\n" + "=" * 60) - print(f"🎉 PERFORMANCE SUMMARY") - print(f"=" * 60) - print(f" Test Items: {num_items}") - print(f" Lookups Tested: {num_lookups}") - print(f"") - print(f" Old Method (directory scan): {old_time:.4f}s") - print(f" New Method (index lookup): {new_time:.4f}s") - print(f"") - print(f" 🚀 Speedup: {speedup:.1f}x faster") - print(f" 📈 Improvement: {improvement:.1f}% faster") - print(f"") - print(f" 📚 Knowledge lookup time: {knowledge_time:.4f}s") - print(f" 📦 Batch retrieval time: {batch_time:.4f}s") - print(f" 🔄 Index rebuild time: {rebuild_time:.4f}s") - - # Show storage statistics - show_storage_statistics(storage) - - print(f"\n✨ Key Benefits:") - print(f" • O(1) lookup time vs O(n) directory scanning") - print(f" • Persistent indexes survive restarts") - print(f" • Automatic index rebuilding for data recovery") - print(f" • Self-healing: removes stale entries automatically") - print(f" • Fallback to directory scan for missing entries") - - print(f"\n📁 Demo data saved in: {demo_dir}") - print(f" Check the index files in: {demo_dir}/extra/") - - except Exception as e: - print(f"\n❌ Error during demonstration: {e}") - raise - - -if __name__ == "__main__": - main() diff --git a/examples/tagger/llm_tagger_example.py b/examples/tagger/llm_tagger_example.py deleted file mode 100644 index 995134d..0000000 --- a/examples/tagger/llm_tagger_example.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Example: LLM tagging for research papers.""" - -import os - -from quantmind.config import LLMTaggerConfig -from quantmind.config.llm import LLMConfig -from quantmind.models.paper import Paper -from quantmind.tagger.llm_tagger import LLMTagger - - -def main(): - """Demonstrate simple LLM tagging.""" - # Create sample paper - paper = Paper( - title="LSTM Networks for High-Frequency Bitcoin Trading", - abstract="""This study implements Long Short-Term Memory (LSTM) neural networks - for predicting Bitcoin price movements in high-frequency trading scenarios. - We use order book data and sentiment analysis from Twitter to train our model, - achieving a Sharpe ratio of 1.8 over a 6-month period.""", - authors=["Alice Johnson", "Bob Chen"], - url="https://example.com/bitcoin-lstm-paper.pdf", - ) - - # Basic usage with defaults - Method 1: Using the convenient create() method - print("=== Basic Usage (using create method) ===") - api_key = os.getenv("OPENAI_API_KEY") - if not api_key: - raise ValueError("OPENAI_API_KEY environment variable is required") - - tagger = LLMTagger( - config=LLMTaggerConfig.create( - model="gpt-4o-mini", - api_key=api_key, - ) - ) - - tagged_paper = tagger.tag_paper(paper) - - print(f"Paper: {tagged_paper.title}") - print(f"Generated Tags: {tagged_paper.tags}") - print(f"Metadata: {tagged_paper.meta_info}") - - # Method 2: Using explicit LLMConfig composition - print("\n=== Alternative Configuration (explicit LLMConfig) ===") - if api_key: - llm_config = LLMConfig( - model="gpt-4o-mini", - api_key=api_key, - temperature=0.3, - ) - - tagger2 = LLMTagger( - config=LLMTaggerConfig( - llm_config=llm_config, - max_tags=5, - ) - ) - - tagged_paper_alt = tagger2.tag_paper(paper) - print(f"Paper: {tagged_paper_alt.title}") - print(f"Generated Tags: {tagged_paper_alt.tags}") - - # Custom configuration with user instructions - print("\n=== Custom Configuration (with user instructions) ===") - if api_key: # Only run if API key is available - custom_tagger = LLMTagger( - config=LLMTaggerConfig.create( - model="gpt-4o-mini", - api_key=api_key, - custom_instructions="Use - to connect tags, like deep-learning.", - max_tags=3, - temperature=0.1, - ) - ) - - # Create another paper - paper2 = Paper( - title="Portfolio Optimization Using Reinforcement Learning", - abstract="We apply deep Q-learning to portfolio allocation in equity markets.", - authors=["Carol Smith"], - ) - - tagged_paper2 = custom_tagger.tag_paper(paper2) - print(f"Paper: {tagged_paper2.title}") - print(f"Generated Tags: {tagged_paper2.tags}") - else: - print("OPENAI_API_KEY not found, skipping custom configuration example") - - # Extract tags from arbitrary text - # print("\n=== Text Analysis ===") - # text = "This paper discusses volatility modeling in forex markets using GARCH models." - # tags = tagger.extract_tags(text, "Volatility Study") - # print(f"Tags from text: {tags}") - - -if __name__ == "__main__": - main() diff --git a/examples/tools/basic_tool.py b/examples/tools/basic_tool.py deleted file mode 100644 index 7074879..0000000 --- a/examples/tools/basic_tool.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Basic example of creating and using a QuantMind tool.""" - -from quantmind.tools import tool, validate_tool_arguments - - -@tool -def calculate_position_value( - price: float, quantity: float, side: str = "long" -) -> float: - """Compute the signed notional value for a position. - - Args: - price (float): Latest unit price in dollars. - quantity (float): Position size in units. - side (str): Trading direction (choices: ["long", "short"]) - - Returns: - float: Signed notional value for the position. - """ - direction = 1 if side == "long" else -1 - return direction * price * quantity - - -def main(): - """Run the tool, validate inputs, and display metadata.""" - payload = {"price": 420.5, "quantity": 2, "side": "long"} - validate_tool_arguments(calculate_position_value, payload) - result = calculate_position_value(**payload) - print(f"Position value: {result}") - print("Tool inputs:") - for name, schema in calculate_position_value.inputs.items(): - print(f" {name}: {schema}") - - -if __name__ == "__main__": - main() diff --git a/examples/tools/pricing_tool.py b/examples/tools/pricing_tool.py deleted file mode 100644 index 136034d..0000000 --- a/examples/tools/pricing_tool.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Lightweight tool calculating portfolio metrics.""" - -from quantmind.tools import tool - - -@tool -def estimate_greeks(delta: float, gamma: float, underlying_move: float) -> dict: - """Estimate option PnL impact from delta and gamma. - - Args: - delta (float): Delta exposure of the option book. - gamma (float): Gamma exposure of the option book. - underlying_move (float): Expected underlying price change. - - Returns: - dict: Estimated delta and gamma contribution. - """ - delta_pnl = delta * underlying_move - gamma_pnl = 0.5 * gamma * underlying_move**2 - return { - "delta_contribution": delta_pnl, - "gamma_contribution": gamma_pnl, - } - - -def main(): - """Run the Greeks estimator with example exposures.""" - pnl = estimate_greeks(delta=1200, gamma=-45, underlying_move=0.8) - print("Estimated option PnL contributions:") - for key, value in pnl.items(): - print(f" {key}: {value:.2f}") - - -if __name__ == "__main__": - main() diff --git a/examples/tools/text_stats_tool.py b/examples/tools/text_stats_tool.py deleted file mode 100644 index 94a014b..0000000 --- a/examples/tools/text_stats_tool.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Text statistics tool demo.""" - -from quantmind.tools import tool - - -@tool -def summarize_text(text: str) -> dict: - """Compute simple statistics for a text snippet. - - Args: - text (str): Input paragraph to analyze. - - Returns: - dict: Contains character and word counts. - """ - words = [word for word in text.split() if word] - return { - "characters": len(text), - "words": len(words), - } - - -def main(): - """Run the text statistics tool with a sample snippet.""" - sample = "Markets rally as investors digest the latest earnings reports." - stats = summarize_text(sample) - print(f"Text: {sample}") - print(f"Characters: {stats['characters']}, Words: {stats['words']}") - - -if __name__ == "__main__": - main() diff --git a/examples/utils/logger_demo.py b/examples/utils/logger_demo.py deleted file mode 100644 index a94d02d..0000000 --- a/examples/utils/logger_demo.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -"""Demonstration of QuantMind's colored logging functionality.""" - -import logging -import time - -from quantmind.utils.logger import ( - get_logger, - configure_logging, - create_demo_logger, -) - - -def basic_logging_demo(): - """Demonstrate basic colored logging.""" - print("=== Basic Colored Logging Demo ===") - - # Get a logger for this module - logger = get_logger(__name__) - - # Log messages at different levels - logger.debug("Debug message - usually for development") - logger.info("Info message - general information") - logger.warning("Warning message - something to pay attention to") - logger.error("Error message - something went wrong") - logger.critical("Critical message - severe error!") - - print() - - -def module_specific_logging(): - """Demonstrate module-specific logging.""" - print("=== Module-Specific Logging ===") - - # Create loggers for different modules - arxiv_logger = get_logger("quantmind.sources.arxiv_source") - parser_logger = get_logger("quantmind.parsers.pdf_parser") - workflow_logger = get_logger("quantmind.workflow.agent") - - # Each logger maintains its own identity - arxiv_logger.info("ArXiv source: Found 10 papers") - parser_logger.warning("PDF parser: Document formatting is unusual") - workflow_logger.error("Workflow agent: Task failed with timeout") - - print() - - -def configuration_demo(): - """Demonstrate different logging configurations.""" - print("=== Configuration Options Demo ===") - - # Configure global logging - print("1. Debug level with colors:") - debug_logger = get_logger("debug_demo", level=logging.DEBUG, use_color=True) - debug_logger.debug("This debug message is now visible") - debug_logger.info("Info message with debug level") - - print("\n2. No colors (simulating non-TTY environment):") - no_color_logger = get_logger("no_color_demo", use_color=False) - no_color_logger.info("This message has no colors") - no_color_logger.error("This error message also has no colors") - - print("\n3. Custom format with file output:") - import tempfile - import os - - temp_file = tempfile.NamedTemporaryFile( - mode="w", suffix=".log", delete=False - ) - temp_file.close() - - try: - file_logger = get_logger("file_demo", file_output=temp_file.name) - file_logger.info("This message goes to both console and file") - file_logger.warning("File logging is useful for production") - - # Show file contents - with open(temp_file.name, "r") as f: - file_contents = f.read() - print(f"\nFile contents:\n{file_contents}") - - finally: - os.unlink(temp_file.name) - - print() - - -def real_world_example(): - """Demonstrate real-world usage scenario.""" - print("=== Real-World Example: ArXiv Source ===") - - # Simulate ArXiv source operations - logger = get_logger("quantmind.sources.arxiv") - - logger.info("Initializing ArXiv source with configuration") - time.sleep(0.5) - - logger.info("Searching for papers: 'machine learning finance'") - time.sleep(0.5) - - logger.warning("Rate limiting: Waiting 1 second between requests") - time.sleep(0.5) - - logger.info("Found 15 papers matching query") - time.sleep(0.5) - - logger.info("Downloading PDF: paper_2301.12345.pdf") - time.sleep(0.5) - - logger.error("Failed to download PDF: Network timeout") - time.sleep(0.5) - - logger.info("Retrying download with exponential backoff") - time.sleep(0.5) - - logger.info("Successfully downloaded 14/15 papers") - - print() - - -def environment_awareness_demo(): - """Demonstrate environment-aware color detection.""" - print("=== Environment Awareness Demo ===") - - import os - import sys - - # Show current environment detection - is_tty = hasattr(sys.stderr, "isatty") and sys.stderr.isatty() - term_type = os.environ.get("TERM", "unknown") - no_color = os.environ.get("NO_COLOR") - - print(f"Terminal detection:") - print(f" - Is TTY: {is_tty}") - print(f" - TERM environment: {term_type}") - print(f" - NO_COLOR set: {no_color is not None}") - - # Auto-detected logger - auto_logger = get_logger("auto_detect") - print(f"\nAuto-detected color usage:") - auto_logger.info("This message uses auto-detected color settings") - - # Test with NO_COLOR environment variable - print(f"\nTesting NO_COLOR environment variable:") - os.environ["NO_COLOR"] = "1" - try: - no_color_auto = get_logger("no_color_auto") - no_color_auto.info("This should have no colors due to NO_COLOR=1") - finally: - del os.environ["NO_COLOR"] - - print() - - -def performance_demo(): - """Demonstrate logging performance with colors.""" - print("=== Performance Demo ===") - - import time - - # Test performance with colors - colored_logger = get_logger("perf_colored", use_color=True) - plain_logger = get_logger("perf_plain", use_color=False) - - # Measure colored logging time - start_time = time.time() - for i in range(100): - colored_logger.info(f"Colored log message {i}") - colored_time = time.time() - start_time - - # Measure plain logging time - start_time = time.time() - for i in range(100): - plain_logger.info(f"Plain log message {i}") - plain_time = time.time() - start_time - - print(f"Performance comparison (100 messages):") - print(f" - Colored logging: {colored_time:.4f} seconds") - print(f" - Plain logging: {plain_time:.4f} seconds") - print( - f" - Overhead: {((colored_time - plain_time) / plain_time * 100):.2f}%" - ) - - print() - - -def main(): - """Run all logging demonstrations.""" - print("QuantMind Colored Logging Demonstration") - print("=" * 50) - print() - - # Configure global logging for demos - configure_logging(level=logging.DEBUG, use_color=True) - - demos = [ - basic_logging_demo, - module_specific_logging, - configuration_demo, - real_world_example, - environment_awareness_demo, - # performance_demo, # Commented out to reduce output - ] - - for demo in demos: - try: - demo() - except Exception as e: - logger = get_logger(__name__) - logger.error(f"Demo failed: {e}") - - print("=== Final Demo: All Log Levels ===") - create_demo_logger() - - print("\n" + "=" * 50) - print("Logging demonstration complete!") - print("The colored output should help distinguish different log levels.") - - -if __name__ == "__main__": - main() diff --git a/examples/utils/prompts.yaml b/examples/utils/prompts.yaml deleted file mode 100644 index 97babb6..0000000 --- a/examples/utils/prompts.yaml +++ /dev/null @@ -1,35 +0,0 @@ -paper_analysis: | - Please analyze the following research paper and provide trading insights: - - Title: {{ title }} - Abstract: {{ abstract }} - Keywords: {{ keywords }} - - Focus on: - - Key quantitative methods and their trading applications - - Potential alpha signals or risk factors identified - - Backtesting considerations and implementation challenges - - Market conditions where this strategy might be most effective - - -# Local prompts for testing relative path loading - -local_prompt: | - This is a local prompt template for testing. - - Custom Parameter: {{ custom_param }} - Local Context: {{ local_context }} - -simple_test: | - Simple test template with {{ variable }}. - -conditional_test: | - {% if condition %} - Condition is true: {{ value }} - {% else %} - Condition is false: {{ value }} - {% endif %} - -nested_test: - key1: "Value 1: {{ var1 }}" - key2: "Value 2: {{ var2 }}" diff --git a/examples/utils/tmp_usage.py b/examples/utils/tmp_usage.py deleted file mode 100644 index 5f7bbe2..0000000 --- a/examples/utils/tmp_usage.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Simple example of QuantMind Template system usage.""" - -from quantmind.utils import T - - -def example_basic_usage(): - """Simple template usage example.""" - # Load and render a prompt template - prompt = T("examples.utils.prompts:paper_analysis").r( - title="Deep Learning for Financial Time Series Forecasting", - abstract="This paper proposes a novel LSTM-based approach for predicting stock prices using high-frequency data.", - keywords="deep learning, LSTM, financial forecasting, high-frequency trading", - ) - - print("Generated Prompt:") - print(prompt) - - -def example_relative_path(): - """Test relative path loading.""" - # Test loading from local.yaml in the same directory - local_prompt = T(".prompts:local_prompt").r( - custom_param="test value", local_context="relative path test" - ) - - print("\nRelative Path Test:") - print(local_prompt) - - # Test nested key access - nested_prompt = T(".prompts:nested_test.key1").r(var1="nested value") - - print("\nNested Key Test:") - print(nested_prompt) - - # Test conditional template - conditional_prompt = T(".prompts:conditional_test").r( - condition=True, value="conditional test" - ) - - print("\nConditional Test:") - print(conditional_prompt) - - -if __name__ == "__main__": - example_basic_usage() - example_relative_path() diff --git a/pyproject.toml b/pyproject.toml index 8496e47..2859069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,12 +45,7 @@ dependencies = [ "ruff", "pytest", "numpy>=2.2.4", - "networkx>=3.4.2", - "scikit-learn>=1.6.1", - "pyvis==0.3.1", - "plotly>=6.0.1", "openai>=1.68.2", - "camel-ai>=0.2.42", "pillow>=10.1.0,<11.0.0", "llama-cloud-services>=0.6.12", "ipykernel>=6.29.5", @@ -63,9 +58,6 @@ dependencies = [ "jinja2>=3.0.0", ] -[project.scripts] -quantmind = "quantmind_cli:main" - [project.optional-dependencies] dev = [ "pytest>=7.0.0", diff --git a/quantmind/brain/__init__.py b/quantmind/brain/__init__.py deleted file mode 100644 index 2538acf..0000000 --- a/quantmind/brain/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .memory import Memory - -__all__ = ["Memory"] diff --git a/quantmind/brain/memory.py b/quantmind/brain/memory.py deleted file mode 100644 index b1725f5..0000000 --- a/quantmind/brain/memory.py +++ /dev/null @@ -1,154 +0,0 @@ -import inspect -from logging import getLogger -from typing import Callable, Type - -from quantmind.models.memory import ( - ActionStep, - MemoryStep, - PlanningStep, - SystemPromptStep, - TaskStep, -) -from quantmind.utils.monitoring import AgentLogger, LogLevel - -logger = getLogger(__name__) - - -class Memory: - """Memory for the brain, containing the system prompt and all steps taken by the brain. - - This class is used to store the agent's steps, including tasks, actions, and planning steps. - It allows for resetting the memory, retrieving succinct or full step information, and replaying - the agent's steps. - - Args: - system_prompt (`str`): System prompt for the agent, which sets the context and instructions - for the agent's behavior. - - **Attributes**: - - **system_prompt** (`SystemPromptStep`) -- System prompt step for the agent. - - **steps** (`list[TaskStep | ActionStep | PlanningStep]`) -- List of steps taken by the - agent, which can include tasks, actions, and planning steps. - """ - - def __init__(self, system_prompt: str): - self.system_prompt: SystemPromptStep = SystemPromptStep( - system_prompt=system_prompt - ) - self.steps: list[TaskStep | ActionStep | PlanningStep] = [] - - def reset(self): - """Reset the agent's memory, clearing all steps and keeping the system prompt.""" - self.steps = [] - - def get_succinct_steps(self) -> list[dict]: - """Return a succinct representation of the agent's steps, excluding model input messages.""" - return [ - { - key: value - for key, value in step.dict().items() - if key != "model_input_messages" - } - for step in self.steps - ] - - def get_full_steps(self) -> list[dict]: - """Return a full representation of the agent's steps, including model input messages.""" - if len(self.steps) == 0: - return [] - return [step.dict() for step in self.steps] - - def replay(self, logger: AgentLogger, detailed: bool = False): - """Prints a pretty replay of the agent's steps. - - Args: - logger (`AgentLogger`): The logger to print replay logs to. - detailed (`bool`, default `False`): If True, also displays the memory at each step. - Defaults to False. - Careful: will increase log length exponentially. Use only for debugging. - """ - logger.console.log("Replaying the agent's steps:") - logger.log_markdown( - title="System prompt", - content=self.system_prompt.system_prompt, - level=LogLevel.ERROR, - ) - for step in self.steps: - if isinstance(step, TaskStep): - logger.log_task(step.task, "", level=LogLevel.ERROR) - elif isinstance(step, ActionStep): - logger.log_rule( - f"Step {step.step_number}", level=LogLevel.ERROR - ) - if detailed and step.model_input_messages is not None: - logger.log_messages( - step.model_input_messages, level=LogLevel.ERROR - ) - if step.model_output is not None: - logger.log_markdown( - title="Agent output:", - content=step.model_output, - level=LogLevel.ERROR, - ) - elif isinstance(step, PlanningStep): - logger.log_rule("Planning step", level=LogLevel.ERROR) - if detailed and step.model_input_messages is not None: - logger.log_messages( - step.model_input_messages, level=LogLevel.ERROR - ) - logger.log_markdown( - title="Agent output:", - content=step.plan, - level=LogLevel.ERROR, - ) - - def return_full_code(self) -> str: - """Returns all code actions from the agent's steps, concatenated as a single script.""" - return "\n\n".join( - [ - step.code_action - for step in self.steps - if isinstance(step, ActionStep) and step.code_action is not None - ] - ) - - -class CallbackRegistry: - """Registry for callbacks that are called at each step of the agent's execution. - - Callbacks are registered by passing a step class and a callback function. - """ - - def __init__(self): - self._callbacks: dict[Type[MemoryStep], list[Callable]] = {} - - def register(self, step_cls: Type[MemoryStep], callback: Callable): - """Register a callback for a step class. - - Args: - step_cls (Type[MemoryStep]): Step class to register the callback for. - callback (Callable): Callback function to register. - """ - if step_cls not in self._callbacks: - self._callbacks[step_cls] = [] - self._callbacks[step_cls].append(callback) - - def callback(self, memory_step, **kwargs): - """Call callbacks registered for a step type. - - Args: - memory_step (MemoryStep): Step to call the callbacks for. - **kwargs: Additional arguments to pass to callbacks that accept them. - Typically, includes the agent instance. - - Notes: - For backwards compatibility, callbacks with a single parameter signature - receive only the memory_step, while callbacks with multiple parameters - receive both the memory_step and any additional kwargs. - """ - # For compatibility with old callbacks that only take the step as an argument - for cls in memory_step.__class__.__mro__: - for cb in self._callbacks.get(cls, []): - cb(memory_step) if len( - inspect.signature(cb).parameters - ) == 1 else cb(memory_step, **kwargs) diff --git a/quantmind/models/memory.py b/quantmind/models/memory.py deleted file mode 100644 index 16b16a9..0000000 --- a/quantmind/models/memory.py +++ /dev/null @@ -1,253 +0,0 @@ -from dataclasses import asdict, dataclass -from logging import getLogger -from typing import TYPE_CHECKING, Any - -from quantmind.models.messages import ( - ChatMessage, - MessageRole, - get_dict_from_nested_dataclasses, -) -from quantmind.utils.agentic_ext import AgentError, make_json_serializable -from quantmind.utils.monitoring import Timing, TokenUsage - -if TYPE_CHECKING: - import PIL.Image - - -__all__ = ["AgentMemory"] - - -logger = getLogger(__name__) - - -@dataclass -class ToolCall: - name: str - arguments: Any - id: str - - def dict(self): - return { - "id": self.id, - "type": "function", - "function": { - "name": self.name, - "arguments": make_json_serializable(self.arguments), - }, - } - - -@dataclass -class MemoryStep: - def dict(self): - return asdict(self) - - def to_messages(self, summary_mode: bool = False) -> list[ChatMessage]: - raise NotImplementedError - - -@dataclass -class ActionStep(MemoryStep): - step_number: int - timing: Timing - model_input_messages: list[ChatMessage] | None = None - tool_calls: list[ToolCall] | None = None - error: AgentError | None = None - model_output_message: ChatMessage | None = None - model_output: str | list[dict[str, Any]] | None = None - code_action: str | None = None - observations: str | None = None - observations_images: list["PIL.Image.Image"] | None = None - action_output: Any = None - token_usage: TokenUsage | None = None - is_final_answer: bool = False - - def dict(self): - # We overwrite the method to parse the tool_calls and action_output manually - return { - "step_number": self.step_number, - "timing": self.timing.dict(), - "model_input_messages": [ - make_json_serializable(get_dict_from_nested_dataclasses(msg)) - for msg in self.model_input_messages - ] - if self.model_input_messages - else None, - "tool_calls": [tc.dict() for tc in self.tool_calls] - if self.tool_calls - else [], - "error": self.error.dict() if self.error else None, - "model_output_message": make_json_serializable( - get_dict_from_nested_dataclasses(self.model_output_message) - ) - if self.model_output_message - else None, - "model_output": self.model_output, - "code_action": self.code_action, - "observations": self.observations, - "observations_images": [ - image.tobytes() for image in self.observations_images - ] - if self.observations_images - else None, - "action_output": make_json_serializable(self.action_output), - "token_usage": asdict(self.token_usage) - if self.token_usage - else None, - "is_final_answer": self.is_final_answer, - } - - def to_messages(self, summary_mode: bool = False) -> list[ChatMessage]: - messages = [] - if self.model_output is not None and not summary_mode: - messages.append( - ChatMessage( - role=MessageRole.ASSISTANT, - content=[ - {"type": "text", "text": self.model_output.strip()} - ], - ) - ) - - if self.tool_calls is not None: - messages.append( - ChatMessage( - role=MessageRole.TOOL_CALL, - content=[ - { - "type": "text", - "text": "Calling tools:\n" - + str([tc.dict() for tc in self.tool_calls]), - } - ], - ) - ) - - if self.observations_images: - messages.append( - ChatMessage( - role=MessageRole.USER, - content=[ - { - "type": "image", - "image": image, - } - for image in self.observations_images - ], - ) - ) - - if self.observations is not None: - messages.append( - ChatMessage( - role=MessageRole.TOOL_RESPONSE, - content=[ - { - "type": "text", - "text": f"Observation:\n{self.observations}", - } - ], - ) - ) - if self.error is not None: - error_message = ( - "Error:\n" - + str(self.error) - + "\nNow let's retry: take care not to repeat previous errors! If you have retried several times, try a completely different approach.\n" - ) - message_content = ( - f"Call id: {self.tool_calls[0].id}\n" if self.tool_calls else "" - ) - message_content += error_message - messages.append( - ChatMessage( - role=MessageRole.TOOL_RESPONSE, - content=[{"type": "text", "text": message_content}], - ) - ) - - return messages - - -@dataclass -class PlanningStep(MemoryStep): - model_input_messages: list[ChatMessage] - model_output_message: ChatMessage - plan: str - timing: Timing - token_usage: TokenUsage | None = None - - def dict(self): - return { - "model_input_messages": [ - make_json_serializable(get_dict_from_nested_dataclasses(msg)) - for msg in self.model_input_messages - ], - "model_output_message": make_json_serializable( - get_dict_from_nested_dataclasses(self.model_output_message) - ), - "plan": self.plan, - "timing": self.timing.dict(), - "token_usage": asdict(self.token_usage) - if self.token_usage - else None, - } - - def to_messages(self, summary_mode: bool = False) -> list[ChatMessage]: - if summary_mode: - return [] - return [ - ChatMessage( - role=MessageRole.ASSISTANT, - content=[{"type": "text", "text": self.plan.strip()}], - ), - ChatMessage( - role=MessageRole.USER, - content=[ - { - "type": "text", - "text": "Now proceed and carry out this plan.", - } - ], - ), - # This second message creates a role change to prevent models models - # from simply continuing the plan message - ] - - -@dataclass -class TaskStep(MemoryStep): - task: str - task_images: list["PIL.Image.Image"] | None = None - - def to_messages(self, summary_mode: bool = False) -> list[ChatMessage]: - content = [{"type": "text", "text": f"New task:\n{self.task}"}] - if self.task_images: - content.extend( - [ - {"type": "image", "image": image} - for image in self.task_images - ] - ) - - return [ChatMessage(role=MessageRole.USER, content=content)] - - -@dataclass -class SystemPromptStep(MemoryStep): - system_prompt: str - - def to_messages(self, summary_mode: bool = False) -> list[ChatMessage]: - if summary_mode: - return [] - return [ - ChatMessage( - role=MessageRole.SYSTEM, - content=[{"type": "text", "text": self.system_prompt}], - ) - ] - - -@dataclass -class FinalAnswerStep(MemoryStep): - output: Any diff --git a/quantmind/models/messages.py b/quantmind/models/messages.py deleted file mode 100644 index fb48b89..0000000 --- a/quantmind/models/messages.py +++ /dev/null @@ -1,447 +0,0 @@ -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import json -import logging -import re -import uuid -from copy import deepcopy -from dataclasses import asdict, dataclass -from enum import Enum -from typing import Any - -from quantmind.tools import Tool -from quantmind.utils.agentic_ext import ( - encode_image_base64, - make_image_url, - parse_json_blob, -) -from quantmind.utils.monitoring import TokenUsage - -logger = logging.getLogger(__name__) - -STRUCTURED_GENERATION_PROVIDERS = ["cerebras", "fireworks-ai"] - - -def get_dict_from_nested_dataclasses(obj, ignore_key=None): - """Convert a nested dataclass to a dictionary.""" - - def convert(obj): - if hasattr(obj, "__dataclass_fields__"): - return { - k: convert(v) for k, v in asdict(obj).items() if k != ignore_key - } - return obj - - return convert(obj) - - -@dataclass -class ChatMessageToolCallFunction: - """Function for a tool call.""" - - arguments: Any - name: str - description: str | None = None - - -@dataclass -class ChatMessageToolCall: - """Tool call for a chat message.""" - - function: ChatMessageToolCallFunction - id: str - type: str - - def __str__(self) -> str: - return f"Call: {self.id}: Calling {str(self.function.name)} with arguments: {str(self.function.arguments)}" - - -class MessageRole(str, Enum): - """Message role.""" - - USER = "user" - ASSISTANT = "assistant" - SYSTEM = "system" - TOOL_CALL = "tool-call" - TOOL_RESPONSE = "tool-response" - - @classmethod - def roles(cls): - return [r.value for r in cls] - - -@dataclass -class ChatMessage: - """Chat message.""" - - role: MessageRole - content: str | list[dict[str, Any]] | None = None - tool_calls: list[ChatMessageToolCall] | None = None - raw: Any | None = None # Stores the raw output from the API - token_usage: TokenUsage | None = None - - def model_dump_json(self): - return json.dumps( - get_dict_from_nested_dataclasses(self, ignore_key="raw") - ) - - @classmethod - def from_dict( - cls, - data: dict, - raw: Any | None = None, - token_usage: TokenUsage | None = None, - ) -> "ChatMessage": - if data.get("tool_calls"): - tool_calls = [ - ChatMessageToolCall( - function=ChatMessageToolCallFunction(**tc["function"]), - id=tc["id"], - type=tc["type"], - ) - for tc in data["tool_calls"] - ] - data["tool_calls"] = tool_calls - return cls( - role=data["role"], - content=data.get("content"), - tool_calls=data.get("tool_calls"), - raw=raw, - token_usage=token_usage, - ) - - def dict(self): - return get_dict_from_nested_dataclasses(self) - - def render_as_markdown(self) -> str: - rendered = str(self.content) or "" - if self.tool_calls: - rendered += "\n".join( - [ - json.dumps( - { - "tool": tool.function.name, - "arguments": tool.function.arguments, - } - ) - for tool in self.tool_calls - ] - ) - return rendered - - -def parse_json_if_needed(arguments: str | dict) -> str | dict: - """Parse a JSON string if needed.""" - if isinstance(arguments, dict): - return arguments - else: - try: - return json.loads(arguments) - except Exception: - return arguments - - -@dataclass -class ChatMessageToolCallStreamDelta: - """Represents a streaming delta for tool calls during generation.""" - - index: int | None = None - id: str | None = None - type: str | None = None - function: ChatMessageToolCallFunction | None = None - - -@dataclass -class ChatMessageStreamDelta: - """Represents a streaming delta for a chat message.""" - - content: str | None = None - tool_calls: list[ChatMessageToolCallStreamDelta] | None = None - token_usage: TokenUsage | None = None - - -def agglomerate_stream_deltas( - stream_deltas: list[ChatMessageStreamDelta], - role: MessageRole = MessageRole.ASSISTANT, -) -> ChatMessage: - """Agglomerate a list of stream deltas into a single stream delta. - - Args: - stream_deltas (`list[ChatMessageStreamDelta]`): List of chat message stream deltas. - role (`MessageRole`, *optional*): Role of the chat message. - - Returns: - `ChatMessage`: Agglomerated chat message. - """ - accumulated_tool_calls: dict[int, ChatMessageToolCallStreamDelta] = {} - accumulated_content = "" - total_input_tokens = 0 - total_output_tokens = 0 - for stream_delta in stream_deltas: - if stream_delta.token_usage: - total_input_tokens += stream_delta.token_usage.input_tokens - total_output_tokens += stream_delta.token_usage.output_tokens - if stream_delta.content: - accumulated_content += stream_delta.content - if stream_delta.tool_calls: - for tool_call_delta in ( - stream_delta.tool_calls - ): # ?ormally there should be only one call at a time - # Extend accumulated_tool_calls list to accommodate the new tool call if needed - if tool_call_delta.index is not None: - if tool_call_delta.index not in accumulated_tool_calls: - accumulated_tool_calls[tool_call_delta.index] = ( - ChatMessageToolCallStreamDelta( - id=tool_call_delta.id, - type=tool_call_delta.type, - function=ChatMessageToolCallFunction( - name="", arguments="" - ), - ) - ) - # Update the tool call at the specific index - tool_call = accumulated_tool_calls[tool_call_delta.index] - if tool_call_delta.id: - tool_call.id = tool_call_delta.id - if tool_call_delta.type: - tool_call.type = tool_call_delta.type - if tool_call_delta.function: - if ( - tool_call_delta.function.name - and len(tool_call_delta.function.name) > 0 - ): - tool_call.function.name = ( - tool_call_delta.function.name - ) - if tool_call_delta.function.arguments: - tool_call.function.arguments += ( - tool_call_delta.function.arguments - ) - else: - raise ValueError( - f"Tool call index is not provided in tool delta: {tool_call_delta}" - ) - - return ChatMessage( - role=role, - content=accumulated_content, - tool_calls=[ - ChatMessageToolCall( - function=ChatMessageToolCallFunction( - name=tool_call_stream_delta.function.name, - arguments=tool_call_stream_delta.function.arguments, - ), - id=tool_call_stream_delta.id or "", - type="function", - ) - for tool_call_stream_delta in accumulated_tool_calls.values() - if tool_call_stream_delta.function - ], - token_usage=TokenUsage( - input_tokens=total_input_tokens, - output_tokens=total_output_tokens, - ), - ) - - -tool_role_conversions = { - MessageRole.TOOL_CALL: MessageRole.ASSISTANT, - MessageRole.TOOL_RESPONSE: MessageRole.USER, -} - - -def get_tool_json_schema(tool: Tool) -> dict: - """Get a JSON schema for a tool.""" - properties = deepcopy(tool.inputs) - required = [] - for key, value in properties.items(): - if value["type"] == "any": - value["type"] = "string" - if not ("nullable" in value and value["nullable"]): - required.append(key) - return { - "type": "function", - "function": { - "name": tool.name, - "description": tool.description, - "parameters": { - "type": "object", - "properties": properties, - "required": required, - }, - }, - } - - -def remove_stop_sequences(content: str, stop_sequences: list[str]) -> str: - """Remove stop sequences from a content.""" - for stop_seq in stop_sequences: - if content[-len(stop_seq) :] == stop_seq: - content = content[: -len(stop_seq)] - return content - - -def get_clean_message_list( - message_list: list[ChatMessage | dict], - role_conversions: dict[MessageRole, MessageRole] | dict[str, str] = {}, - convert_images_to_image_urls: bool = False, - flatten_messages_as_text: bool = False, -) -> list[dict[str, Any]]: - """Get a clean message list. - - Creates a list of messages to give as input to the LLM. - These messages are dictionaries and chat - template compatible with transformers LLM chat template. - Subsequent messages with the same role will be concatenated to a single message. - - Args: - message_list (`list[ChatMessage | dict]`): List of chat messages. Mixed types are allowed. - role_conversions (`dict[MessageRole, MessageRole]`, *optional* ): Mapping to convert roles. - convert_images_to_image_urls (`bool`, default `False`): - Whether to convert images to imageURLs. - flatten_messages_as_text (`bool`, default `False`): Whether to flatten messages as text. - """ - output_message_list: list[dict[str, Any]] = [] - message_list = deepcopy(message_list) # Avoid modifying the original list - for message in message_list: - if isinstance(message, dict): - message = ChatMessage.from_dict(message) - role = message.role - if role not in MessageRole.roles(): - raise ValueError( - f"Incorrect role {role}, only {MessageRole.roles()} are supported for now." - ) - - if role in role_conversions: - message.role = role_conversions[role] # type: ignore - # encode images if needed - if isinstance(message.content, list): - for element in message.content: - assert isinstance(element, dict), ( - "Error: this element should be a dict:" + str(element) - ) - if element["type"] == "image": - assert ( - not flatten_messages_as_text - ), f"Cannot use images with {flatten_messages_as_text=}" - if convert_images_to_image_urls: - element.update( - { - "type": "image_url", - "image_url": { - "url": make_image_url( - encode_image_base64( - element.pop("image") - ) - ) - }, - } - ) - else: - element["image"] = encode_image_base64(element["image"]) - - if ( - len(output_message_list) > 0 - and message.role == output_message_list[-1]["role"] - ): - assert isinstance(message.content, list), ( - "Error: wrong content:" + str(message.content) - ) - if flatten_messages_as_text: - output_message_list[-1]["content"] += ( - "\n" + message.content[0]["text"] - ) - else: - for el in message.content: - if ( - el["type"] == "text" - and output_message_list[-1]["content"][-1]["type"] - == "text" - ): - # Merge consecutive text messages rather than creating new ones - output_message_list[-1]["content"][-1]["text"] += ( - "\n" + el["text"] - ) - else: - output_message_list[-1]["content"].append(el) - else: - if flatten_messages_as_text: - content = message.content[0]["text"] - else: - content = message.content - output_message_list.append( - { - "role": message.role, - "content": content, - } - ) - return output_message_list - - -def get_tool_call_from_text( - text: str, tool_name_key: str, tool_arguments_key: str -) -> ChatMessageToolCall: - """Get a tool call from a text.""" - tool_call_dictionary, _ = parse_json_blob(text) - try: - tool_name = tool_call_dictionary[tool_name_key] - except Exception as e: - raise ValueError( - f"Tool call needs to have a key '{tool_name_key}'. Got keys: {list(tool_call_dictionary.keys())} instead" - ) from e - tool_arguments = tool_call_dictionary.get(tool_arguments_key, None) - if isinstance(tool_arguments, str): - tool_arguments = parse_json_if_needed(tool_arguments) - return ChatMessageToolCall( - id=str(uuid.uuid4()), - type="function", - function=ChatMessageToolCallFunction( - name=tool_name, arguments=tool_arguments - ), - ) - - -def supports_stop_parameter(model_id: str) -> bool: - """Check if the model supports the `stop` parameter. - - Not supported with reasoning models openai/o3, openai/o4-mini, and - the openai/gpt-5 series (and their versioned variants). - - Args: - model_id (`str`): Model identifier (e.g. "openai/o3", "o4-mini-2025-04-16") - - Returns: - bool: True if the model supports the stop parameter, False otherwise - """ - model_name = model_id.split("/")[-1] - # o3, o4-mini, grok-3-mini, grok-4, grok-code-fast and the gpt-5 series - # (including versioned variants, o3-2025-04-16) don't support stop parameter - openai_model_pattern = r"(o3[-\d]*|o4-mini[-\d]*|gpt-5(-mini|-nano)?[-\d]*)" - grok_model_pattern = ( - r"([a-zA-Z]+\.)?(grok-3-mini|grok-4|grok-code-fast)(-[A-Za-z0-9]*)?" - ) - pattern = rf"^({openai_model_pattern}|{grok_model_pattern})$" - - return not re.match(pattern, model_name) - - -class _ParameterRemove: - """Sentinel value to indicate a parameter should be removed.""" - - def __repr__(self): - return "REMOVE_PARAMETER" - - -# Singleton instance for removing parameters -REMOVE_PARAMETER = _ParameterRemove() diff --git a/quantmind/storage/__init__.py b/quantmind/storage/__init__.py deleted file mode 100644 index 8f7e057..0000000 --- a/quantmind/storage/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Storage systems for QuantMind knowledge base.""" - -from quantmind.storage.base import BaseStorage -from quantmind.storage.local_storage import LocalStorage - -__all__ = ["BaseStorage", "LocalStorage"] diff --git a/quantmind/storage/base.py b/quantmind/storage/base.py deleted file mode 100644 index 4f77d12..0000000 --- a/quantmind/storage/base.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Base storage interface for QuantMind knowledge base.""" - -from abc import ABC, abstractmethod -from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional - -from quantmind.models import KnowledgeItem, Paper -from quantmind.utils.logger import get_logger - -logger = get_logger(__name__) - - -class BaseStorage(ABC): - """Abstract base class for knowledge storage backends. - - Manages four types of data: - - Raw Files: PDFs, markdown files, etc. - - Knowledges: KnowledgeItem objects as JSON - - Embeddings: Embedding vectors as arrays - - Extra: Additional metadata and hashes - """ - - # Raw Files Management - @abstractmethod - def store_raw_file( - self, - file_id: str, - file_path: Optional[Path] = None, - content: Optional[bytes] = None, - file_extension: str = "", - ) -> str: - """Store a raw file (PDF, markdown, etc.). - - Args: - file_id: Unique identifier for the file - file_path: Path to existing file to copy (mutually exclusive with content) - content: Raw bytes content to write directly (mutually exclusive with file_path) - file_extension: File extension when using content (e.g., '.pdf', '.txt') - - Returns: - Path to stored file - - Raises: - ValueError: If both file_path and content are provided or both are None - """ - pass - - @abstractmethod - def get_raw_file(self, file_id: str) -> Optional[Path]: - """Get path to a raw file.""" - pass - - @abstractmethod - def delete_raw_file(self, file_id: str) -> bool: - """Delete a raw file.""" - pass - - # Knowledge Items Management - @abstractmethod - def store_knowledge(self, knowledge: KnowledgeItem) -> str: - """Store a knowledge item.""" - pass - - @abstractmethod - def get_knowledge(self, knowledge_id: str) -> Optional[KnowledgeItem]: - """Get a knowledge item by ID.""" - pass - - @abstractmethod - def delete_knowledge(self, knowledge_id: str) -> bool: - """Delete a knowledge item.""" - pass - - # Embeddings Management - @abstractmethod - def store_embedding( - self, knowledge_id: str, embedding: List[float], model: str - ) -> str: - """Store an embedding vector.""" - pass - - @abstractmethod - def get_embedding(self, knowledge_id: str) -> Optional[Dict[str, Any]]: - """Get embedding data for a knowledge item.""" - pass - - @abstractmethod - def delete_embedding(self, knowledge_id: str) -> bool: - """Delete an embedding.""" - pass - - # Extra Data Management - @abstractmethod - def store_extra(self, key: str, data: Any) -> str: - """Store extra data (hashes, metadata, etc.).""" - pass - - @abstractmethod - def get_extra(self, key: str) -> Optional[Any]: - """Get extra data by key.""" - pass - - @abstractmethod - def delete_extra(self, key: str) -> bool: - """Delete extra data.""" - pass - - # Specialized Knowledge Item Processing - def process_knowledge(self, knowledge: KnowledgeItem) -> str: - """Store knowledge item with specialized processing based on type. - - This method provides type-specific handling: - - Paper: Download PDF if URL available and not already stored - - Other types: Basic storage - - Args: - knowledge: KnowledgeItem instance to store - - Returns: - Knowledge ID after storage - """ - knowledge_id = knowledge.get_primary_id() - - # Store the knowledge item first - stored_id = self.store_knowledge(knowledge) - - # Type-specific processing - if isinstance(knowledge, Paper): - logger.info( - f"Storage Processing paper {knowledge.get_primary_id()}" - ) - self._handle_paper_files(knowledge) - - return stored_id - - def process_knowledges(self, knowledges: List[KnowledgeItem]) -> List[str]: - """Process a list of knowledge items.""" - return [self.process_knowledge(knowledge) for knowledge in knowledges] - - def _handle_paper_files(self, paper: Paper) -> None: - """Handle file operations for Paper objects. - - Args: - paper: Paper instance to process - """ - paper_id = paper.get_primary_id() - - # Check if PDF file already exists - existing_pdf = self.get_raw_file(paper_id) - if existing_pdf and existing_pdf.exists(): - return # File already exists - - # Try to download PDF if URL is available - if paper.pdf_url: - try: - content = self._download_file_content(paper.pdf_url) - if content: - self.store_raw_file( - file_id=paper_id, content=content, file_extension=".pdf" - ) - except Exception as e: - # Log error but don't fail the entire operation - logger.error( - f"Failed to download PDF for {paper.get_primary_id()}: {e}" - ) - - def _download_file_content( - self, url: str, timeout: Optional[int] = None - ) -> Optional[bytes]: - """Download file content from URL. - - Args: - url: URL to download from - timeout: Timeout in seconds (uses config if None) - - Returns: - File content as bytes or None if failed - """ - # Use config timeout if not provided - if timeout is None: - timeout = getattr(self, "config", None) - timeout = ( - getattr(timeout, "download_timeout", 30) if timeout else 30 - ) - - try: - import requests - - response = requests.get(url, timeout=timeout) - response.raise_for_status() - return response.content - except Exception: - return None - - # Utility Methods - def get_all_knowledges(self) -> Iterator[KnowledgeItem]: - """Get all knowledge items.""" - return iter(self.search_knowledges(limit=None)) - - def knowledge_exists(self, knowledge_id: str) -> bool: - """Check if a knowledge item exists.""" - return self.get_knowledge(knowledge_id) is not None - - def get_storage_info(self) -> Dict[str, Any]: - """Get storage information.""" - return { - "type": self.__class__.__name__, - "knowledge_count": len(list(self.get_all_knowledges())), - } - - def __str__(self) -> str: - """String representation.""" - return f"{self.__class__.__name__}()" diff --git a/quantmind/storage/local_storage.py b/quantmind/storage/local_storage.py deleted file mode 100644 index 0f72f8f..0000000 --- a/quantmind/storage/local_storage.py +++ /dev/null @@ -1,558 +0,0 @@ -"""Local file-based storage implementation for QuantMind.""" - -import json -import shutil -from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional - -from quantmind.config import LocalStorageConfig -from quantmind.models import KnowledgeItem -from quantmind.utils.logger import get_logger - -from .base import BaseStorage - -logger = get_logger(__name__) - - -class LocalStorage(BaseStorage): - """Local file-based storage implementation. - - Organizes data into four directories: - - raw_files/: Original files (PDFs, markdown, etc.) - - knowledges/: KnowledgeItem objects as JSON - - embeddings/: Embedding vectors as JSON arrays - - extra/: Additional metadata and hashes - - Uses efficient indexing system for fast lookups: - - raw_files_index.json: file_id -> {"path": "xxx", "extension": "yyy"} - - knowledges_index.json: knowledge_id -> {"path": "xxx"} - - embeddings_index.json: knowledge_id -> {"path": "xxx"} - """ - - def __init__(self, config: LocalStorageConfig): - """Initialize local storage. - - Args: - config: LocalStorageConfig instance - """ - self.config = config - self.config.model_post_init(None) # Ensure directories exist - - # Initialize indexes - self._raw_files_index: Dict[str, Dict[str, str]] = {} - self._knowledges_index: Dict[str, Dict[str, str]] = {} - self._embeddings_index: Dict[str, Dict[str, str]] = {} - - self._load_indexes() - logger.info(f"LocalStorage initialized at {self.config.storage_dir}") - - def _get_index_path(self, index_type: str) -> Path: - """Get path to index file.""" - return self.config.extra_dir / f"{index_type}_index.json" - - def _load_indexes(self) -> None: - """Load all indexes from disk.""" - self._load_index("raw_files") - self._load_index("knowledges") - self._load_index("embeddings") - - def _load_index(self, index_type: str) -> None: - """Load specific index from disk.""" - index_path = self._get_index_path(index_type) - try: - if index_path.exists(): - with open(index_path, "r", encoding="utf-8") as f: - index_data = json.load(f) - - if index_type == "raw_files": - self._raw_files_index = index_data - elif index_type == "knowledges": - self._knowledges_index = index_data - elif index_type == "embeddings": - self._embeddings_index = index_data - else: - # Build index from existing files if index doesn't exist - self._rebuild_index(index_type) - - except Exception as e: - logger.warning( - f"Failed to load {index_type} index: {e}, rebuilding..." - ) - self._rebuild_index(index_type) - - def _save_index(self, index_type: str) -> None: - """Save specific index to disk.""" - index_path = self._get_index_path(index_type) - try: - if index_type == "raw_files": - index_data = self._raw_files_index - elif index_type == "knowledges": - index_data = self._knowledges_index - elif index_type == "embeddings": - index_data = self._embeddings_index - else: - return - - with open(index_path, "w", encoding="utf-8") as f: - json.dump(index_data, f, indent=2, ensure_ascii=False) - - except Exception as e: - logger.error(f"Failed to save {index_type} index: {e}") - - def _rebuild_index(self, index_type: str) -> None: - """Rebuild index by scanning directory.""" - logger.info(f"Rebuilding {index_type} index...") - - if index_type == "raw_files": - self._raw_files_index.clear() - if self.config.raw_files_dir.exists(): - for file_path in self.config.raw_files_dir.iterdir(): - if file_path.is_file(): - # Extract file_id from filename (everything before last dot) - file_id = file_path.stem - self._raw_files_index[file_id] = { - "path": str( - file_path.relative_to(self.config.storage_dir) - ), - "extension": file_path.suffix, - } - - elif index_type == "knowledges": - self._knowledges_index.clear() - if self.config.knowledges_dir.exists(): - for file_path in self.config.knowledges_dir.glob("*.json"): - knowledge_id = file_path.stem - self._knowledges_index[knowledge_id] = { - "path": str( - file_path.relative_to(self.config.storage_dir) - ) - } - - elif index_type == "embeddings": - self._embeddings_index.clear() - if self.config.embeddings_dir.exists(): - for file_path in self.config.embeddings_dir.glob("*.json"): - knowledge_id = file_path.stem - self._embeddings_index[knowledge_id] = { - "path": str( - file_path.relative_to(self.config.storage_dir) - ) - } - - self._save_index(index_type) - logger.info( - f"Rebuilt {index_type} index with {len(getattr(self, f'_{index_type}_index'))} entries" - ) - - def rebuild_all_indexes(self) -> None: - """Rebuild all indexes from scratch.""" - logger.info("Rebuilding all indexes...") - self._rebuild_index("raw_files") - self._rebuild_index("knowledges") - self._rebuild_index("embeddings") - logger.info("All indexes rebuilt successfully") - - # Raw Files Management - def store_raw_file( - self, - file_id: str, - file_path: Optional[Path] = None, - content: Optional[bytes] = None, - file_extension: str = "", - ) -> str: - """Store a raw file by copying or writing content directly.""" - try: - # Validate input parameters - if file_path is not None and content is not None: - raise ValueError("Cannot specify both file_path and content") - if file_path is None and content is None: - raise ValueError("Must specify either file_path or content") - - # Determine target path and extension - if file_path is not None: - # Copy from existing file - source_path = Path(file_path) - if not source_path.exists(): - raise FileNotFoundError( - f"Source file not found: {file_path}" - ) - - target_path = ( - self.config.raw_files_dir / f"{file_id}{source_path.suffix}" - ) - extension = source_path.suffix - - # Copy file - shutil.copy2(source_path, target_path) - logger.debug( - f"Stored raw file {file_id} by copying from {file_path}" - ) - - else: - # Write content directly - if not file_extension: - file_extension = ".bin" # Default extension - if not file_extension.startswith("."): - file_extension = f".{file_extension}" - - target_path = ( - self.config.raw_files_dir / f"{file_id}{file_extension}" - ) - extension = file_extension - - # Write content to file - with open(target_path, "wb") as f: - f.write(content) - logger.debug( - f"Stored raw file {file_id} by writing content directly" - ) - - # Update index - self._raw_files_index[file_id] = { - "path": str(target_path.relative_to(self.config.storage_dir)), - "extension": extension, - } - self._save_index("raw_files") - - return str(target_path) - - except Exception as e: - logger.error(f"Failed to store raw file {file_id}: {e}") - raise - - def get_raw_file(self, file_id: str) -> Optional[Path]: - """Get path to a raw file using efficient index lookup.""" - try: - # Fast index lookup - if file_id in self._raw_files_index: - relative_path = self._raw_files_index[file_id]["path"] - file_path = self.config.storage_dir / relative_path - - if file_path.exists(): - return file_path - else: - # File was deleted externally, remove from index - logger.warning( - f"Raw file {file_id} in index but missing on disk, removing from index" - ) - del self._raw_files_index[file_id] - self._save_index("raw_files") - return None - - # Fallback to directory scan if not in index - for file_path in self.config.raw_files_dir.glob(f"{file_id}.*"): - if file_path.is_file(): - # Add to index for future lookups - self._raw_files_index[file_id] = { - "path": str( - file_path.relative_to(self.config.storage_dir) - ), - "extension": file_path.suffix, - } - self._save_index("raw_files") - return file_path - - return None - - except Exception as e: - logger.error(f"Failed to get raw file {file_id}: {e}") - return None - - def delete_raw_file(self, file_id: str) -> bool: - """Delete a raw file and update index.""" - try: - file_path = self.get_raw_file(file_id) - if file_path and file_path.exists(): - file_path.unlink() - - # Remove from index - if file_id in self._raw_files_index: - del self._raw_files_index[file_id] - self._save_index("raw_files") - - logger.debug(f"Deleted raw file {file_id}") - return True - return False - - except Exception as e: - logger.error(f"Failed to delete raw file {file_id}: {e}") - return False - - # Knowledge Items Management - def store_knowledge(self, knowledge: KnowledgeItem) -> str: - """Store a knowledge item as JSON and update index.""" - try: - knowledge_id = knowledge.get_primary_id() - file_path = self.config.knowledges_dir / f"{knowledge_id}.json" - - # Save to JSON file - with open(file_path, "w", encoding="utf-8") as f: - json.dump( - knowledge.model_dump(), - f, - indent=2, - ensure_ascii=False, - default=str, - ) - - # Update index - self._knowledges_index[knowledge_id] = { - "path": str(file_path.relative_to(self.config.storage_dir)) - } - self._save_index("knowledges") - - logger.debug(f"Stored knowledge {knowledge_id} at {file_path}") - return knowledge_id - - except Exception as e: - logger.error( - f"Failed to store knowledge {knowledge.get_primary_id()}: {e}" - ) - raise - - def get_knowledge_path(self, knowledge_id: str) -> Optional[Path]: - """Get the path to a knowledge item by ID using efficient index lookup.""" - if knowledge_id in self._knowledges_index: - relative_path = self._knowledges_index[knowledge_id]["path"] - return self.config.storage_dir / relative_path - return None - - def get_knowledge(self, knowledge_id: str) -> Optional[KnowledgeItem]: - """Get a knowledge item by ID using efficient index lookup.""" - try: - # Fast index lookup - if knowledge_id in self._knowledges_index: - relative_path = self._knowledges_index[knowledge_id]["path"] - file_path = self.config.storage_dir / relative_path - - if file_path.exists(): - with open(file_path, "r", encoding="utf-8") as f: - data = json.load(f) - return KnowledgeItem(**data) - else: - # File was deleted externally, remove from index - logger.warning( - f"Knowledge {knowledge_id} in index but missing on disk, removing from index" - ) - del self._knowledges_index[knowledge_id] - self._save_index("knowledges") - return None - - # Fallback to direct file check - file_path = self.config.knowledges_dir / f"{knowledge_id}.json" - if file_path.exists(): - # Add to index for future lookups - self._knowledges_index[knowledge_id] = { - "path": str(file_path.relative_to(self.config.storage_dir)) - } - self._save_index("knowledges") - - with open(file_path, "r", encoding="utf-8") as f: - data = json.load(f) - return KnowledgeItem(**data) - - return None - - except Exception as e: - logger.error(f"Failed to get knowledge {knowledge_id}: {e}") - return None - - def delete_knowledge(self, knowledge_id: str) -> bool: - """Delete a knowledge item and update index.""" - try: - # Check index first for fast lookup - if knowledge_id in self._knowledges_index: - relative_path = self._knowledges_index[knowledge_id]["path"] - file_path = self.config.storage_dir / relative_path - else: - file_path = self.config.knowledges_dir / f"{knowledge_id}.json" - - if file_path.exists(): - file_path.unlink() - - # Remove from index - if knowledge_id in self._knowledges_index: - del self._knowledges_index[knowledge_id] - self._save_index("knowledges") - - logger.debug(f"Deleted knowledge {knowledge_id}") - return True - return False - - except Exception as e: - logger.error(f"Failed to delete knowledge {knowledge_id}: {e}") - return False - - # Embeddings Management - def store_embedding( - self, knowledge_id: str, embedding: List[float], model: str - ) -> str: - """Store an embedding vector and update index.""" - try: - file_path = self.config.embeddings_dir / f"{knowledge_id}.json" - - embedding_data = { - "knowledge_id": knowledge_id, - "embedding": embedding, - "model": model, - "created_at": str(Path().stat().st_mtime), # Simple timestamp - } - - with open(file_path, "w", encoding="utf-8") as f: - json.dump(embedding_data, f, indent=2) - - # Update index - self._embeddings_index[knowledge_id] = { - "path": str(file_path.relative_to(self.config.storage_dir)) - } - self._save_index("embeddings") - - logger.debug( - f"Stored embedding for {knowledge_id} (model: {model})" - ) - return knowledge_id - - except Exception as e: - logger.error(f"Failed to store embedding for {knowledge_id}: {e}") - raise - - def get_embedding(self, knowledge_id: str) -> Optional[Dict[str, Any]]: - """Get embedding data for a knowledge item using efficient index lookup.""" - try: - # Fast index lookup - if knowledge_id in self._embeddings_index: - relative_path = self._embeddings_index[knowledge_id]["path"] - file_path = self.config.storage_dir / relative_path - - if file_path.exists(): - with open(file_path, "r", encoding="utf-8") as f: - return json.load(f) - else: - # File was deleted externally, remove from index - logger.warning( - f"Embedding {knowledge_id} in index but missing on disk, removing from index" - ) - del self._embeddings_index[knowledge_id] - self._save_index("embeddings") - return None - - # Fallback to direct file check - file_path = self.config.embeddings_dir / f"{knowledge_id}.json" - if file_path.exists(): - # Add to index for future lookups - self._embeddings_index[knowledge_id] = { - "path": str(file_path.relative_to(self.config.storage_dir)) - } - self._save_index("embeddings") - - with open(file_path, "r", encoding="utf-8") as f: - return json.load(f) - - return None - - except Exception as e: - logger.error(f"Failed to get embedding for {knowledge_id}: {e}") - return None - - def delete_embedding(self, knowledge_id: str) -> bool: - """Delete an embedding and update index.""" - try: - # Check index first for fast lookup - if knowledge_id in self._embeddings_index: - relative_path = self._embeddings_index[knowledge_id]["path"] - file_path = self.config.storage_dir / relative_path - else: - file_path = self.config.embeddings_dir / f"{knowledge_id}.json" - - if file_path.exists(): - file_path.unlink() - - # Remove from index - if knowledge_id in self._embeddings_index: - del self._embeddings_index[knowledge_id] - self._save_index("embeddings") - - logger.debug(f"Deleted embedding for {knowledge_id}") - return True - return False - - except Exception as e: - logger.error(f"Failed to delete embedding for {knowledge_id}: {e}") - return False - - # Extra Data Management - def store_extra(self, key: str, data: Any) -> str: - """Store extra data (hashes, metadata, etc.).""" - try: - file_path = self.config.extra_dir / f"{key}.json" - - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False, default=str) - - logger.debug(f"Stored extra data for key: {key}") - return key - - except Exception as e: - logger.error(f"Failed to store extra data for {key}: {e}") - raise - - def get_extra(self, key: str) -> Optional[Any]: - """Get extra data by key.""" - try: - file_path = self.config.extra_dir / f"{key}.json" - if not file_path.exists(): - return None - - with open(file_path, "r", encoding="utf-8") as f: - return json.load(f) - - except Exception as e: - logger.error(f"Failed to get extra data for {key}: {e}") - return None - - def delete_extra(self, key: str) -> bool: - """Delete extra data.""" - try: - file_path = self.config.extra_dir / f"{key}.json" - if file_path.exists(): - file_path.unlink() - logger.debug(f"Deleted extra data for key: {key}") - return True - return False - - except Exception as e: - logger.error(f"Failed to delete extra data for {key}: {e}") - return False - - # Utility Methods - def get_all_knowledges(self) -> Iterator[KnowledgeItem]: - """Get all knowledge items using efficient index.""" - for knowledge_id in self._knowledges_index.keys(): - knowledge = self.get_knowledge(knowledge_id) - if knowledge: - yield knowledge - - def get_storage_info(self) -> Dict[str, Any]: - """Get storage information including index statistics.""" - return { - "type": self.__class__.__name__, - "config": self.config.model_dump(), - "storage_dir": str(self.config.storage_dir), - "knowledge_count": len(self._knowledges_index), - "raw_files_count": len(self._raw_files_index), - "embeddings_count": len(self._embeddings_index), - "indexes": { - "raw_files": { - "entries": len(self._raw_files_index), - "index_file": str(self._get_index_path("raw_files")), - }, - "knowledges": { - "entries": len(self._knowledges_index), - "index_file": str(self._get_index_path("knowledges")), - }, - "embeddings": { - "entries": len(self._embeddings_index), - "index_file": str(self._get_index_path("embeddings")), - }, - }, - } diff --git a/quantmind/tagger/__init__.py b/quantmind/tagger/__init__.py deleted file mode 100644 index ae42963..0000000 --- a/quantmind/tagger/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Content tagging and classification components.""" - -from quantmind.tagger.base import BaseTagger -from quantmind.tagger.llm_tagger import LLMTagger - -__all__ = ["BaseTagger", "LLMTagger"] diff --git a/quantmind/tagger/base.py b/quantmind/tagger/base.py deleted file mode 100644 index 3b8a5c2..0000000 --- a/quantmind/tagger/base.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Base tagger interface for content classification.""" - -from abc import ABC, abstractmethod -from typing import Dict, List, Any, Optional - -from quantmind.models.paper import Paper - - -class BaseTagger(ABC): - """Abstract base class for content tagging and classification. - - Defines the interface for extracting tags, categories, and classifications - from research papers and other content. - """ - - def __init__(self, config: Optional[Dict[str, Any]] = None): - """Initialize tagger with configuration. - - Args: - config: Tagger-specific configuration - """ - self.config = config or {} - self.name = self.__class__.__name__.lower().replace("tagger", "") - - @abstractmethod - def tag_paper(self, paper: Paper) -> Paper: - """Add tags and categories to a paper. - - Args: - paper: Paper object to tag - - Returns: - Paper object with added tags and categories - """ - pass - - def tag_papers(self, papers: List[Paper]) -> List[Paper]: - """Tag multiple papers. - - Args: - papers: List of Paper objects to tag - - Returns: - List of tagged Paper objects - """ - return [self.tag_paper(paper) for paper in papers] - - @abstractmethod - def extract_tags(self, text: str, title: str = "") -> List[str]: - """Extract tags from text content. - - Args: - text: Text content to analyze - title: Optional title for additional context - - Returns: - List of tag strings - """ - pass - - def validate_tags(self, tags: List[str]) -> List[str]: - """Validate and clean extracted tags. - - Args: - tags: List of raw tags - - Returns: - List of validated and cleaned tags - """ - valid_tags = [] - for tag in tags: - if isinstance(tag, str) and len(tag.strip()) > 0: - cleaned_tag = tag.strip().lower() - if cleaned_tag not in valid_tags: - valid_tags.append(cleaned_tag) - return valid_tags - - def validate_categories(self, categories: List[str]) -> List[str]: - """Validate and clean extracted categories. - - Args: - categories: List of raw categories - - Returns: - List of validated and cleaned categories - """ - return self.validate_tags(categories) # Same validation logic - - def get_tagger_info(self) -> Dict[str, Any]: - """Get information about this tagger. - - Returns: - Dictionary with tagger metadata - """ - return { - "name": self.name, - "type": self.__class__.__name__, - "config": self.config, - } - - def __str__(self) -> str: - """String representation.""" - return f"{self.__class__.__name__}(name='{self.name}')" diff --git a/quantmind/tagger/llm_tagger.py b/quantmind/tagger/llm_tagger.py deleted file mode 100644 index c214ed2..0000000 --- a/quantmind/tagger/llm_tagger.py +++ /dev/null @@ -1,267 +0,0 @@ -"""Simple LLM-based tagger for financial research papers using LLMBlock.""" - -import json -from typing import List - -from quantmind.config import LLMTaggerConfig -from quantmind.llm import create_llm_block -from quantmind.models import Paper -from quantmind.utils.logger import get_logger - -from .base import BaseTagger - -logger = get_logger(__name__) - - -class LLMTagger(BaseTagger): - """Simple LLM-based tagger for financial research papers. - - Uses LLMBlock to generate relevant tags for quantitative finance papers. - """ - - def __init__( - self, - config: LLMTaggerConfig = None, - ): - """Initialize LLM tagger. - - Args: - config: Configuration for the LLM tagger - """ - super().__init__() - self.config = config or LLMTaggerConfig() - - # Create LLMBlock directly from the embedded LLMConfig - try: - self.llm_block = create_llm_block(self.config.llm_config) - logger.info( - f"Initialized LLM tagger with model: {self.config.llm_config.model}" - ) - except Exception as e: - logger.error(f"Failed to initialize LLM block: {e}") - self.llm_block = None - - def tag_paper(self, paper: Paper) -> Paper: - """Generate tags for a paper using LLM analysis. - - Args: - paper: Paper object to tag - - Returns: - Paper object with added tags - """ - if not self.llm_block: - logger.warning("No LLM block available, skipping tagging") - return paper - - try: - # Get paper content for analysis - content = self._prepare_content(paper) - - # Generate tags using LLM - tags = self._generate_tags(content) - - # Add tags to paper - for tag in tags: - paper.add_tag(tag) - - # Store tagging metadata - paper.meta_info.update( - { - "tagger": "llm_tagger", - "model_used": self.config.llm_config.model, - "tags_generated": len(tags), - } - ) - - logger.info(f"Generated {len(tags)} tags for paper: {paper.title}") - - except Exception as e: - logger.error(f"Error tagging paper {paper.get_primary_id()}: {e}") - - return paper - - def _prepare_content(self, paper: Paper) -> str: - """Prepare paper content for LLM analysis. - - Args: - paper: Paper object - - Returns: - Formatted content string - """ - content_parts = [] - - if paper.title: - content_parts.append(f"Title: {paper.title}") - - if paper.abstract: - content_parts.append(f"Abstract: {paper.abstract}") - - # Use first max_tokens characters of content to stay within token limits - if paper.content: - content_parts.append( - f"Content: {paper.content[: self.config.llm_config.max_tokens]}..." - ) - - return "\n\n".join(content_parts) - - def _generate_tags(self, content: str) -> List[str]: - """Generate tags using LLM. - - Args: - content: Paper content to analyze - - Returns: - List of generated tags - """ - prompt = self._build_prompt(content) - - try: - response = self.llm_block.generate_text(prompt) - - if not response: - logger.error("No response from LLM") - return [] - - logger.debug(f"LLM response: {response}") - - # Parse tags from response - tags = self._parse_tags(response) - - # Limit to max_tags - return tags[: self.config.max_tags] - - except Exception as e: - logger.error(f"Error generating tags: {e}") - return [] - - def _build_prompt(self, content: str) -> str: - """Build prompt for tag generation. - - Args: - content: Paper content - - Returns: - Formatted prompt - """ - if self.config.custom_prompt: - return self.config.custom_prompt.format( - content=content, max_tags=self.config.max_tags - ) - - # Default prompt for quantitative finance papers - return f"""Analyze this quantitative finance research paper and generate {self.config.max_tags} relevant tags. - -Paper Content: -{content} - -Generate tags that capture the key aspects like: -- Market types (equity, forex, crypto, bonds) -- Methods (machine learning, deep learning, statistical) -- Applications (trading, risk management, portfolio optimization) -- Data types (price data, news, sentiment) -- Techniques (LSTM, transformers, regression) - -Return only a JSON list of tags, no other text: -["tag1", "tag2", "tag3", "tag4", "tag5"]""" - - def _parse_tags(self, response: str) -> List[str]: - """Parse tags from LLM response. - - Args: - response: Raw LLM response - - Returns: - List of parsed tags - """ - try: - # Try to find JSON array in response - start_idx = response.find("[") - end_idx = response.rfind("]") + 1 - - if start_idx != -1 and end_idx > start_idx: - json_str = response[start_idx:end_idx] - tags = json.loads(json_str) - - if isinstance(tags, list): - # Clean and validate tags - cleaned_tags = [] - for tag in tags: - if isinstance(tag, str) and tag.strip(): - cleaned_tags.append(tag.strip().lower()) - - return cleaned_tags - - except (json.JSONDecodeError, ValueError) as e: - logger.warning(f"Failed to parse JSON tags: {e}") - - # Fallback: try to extract tags from plain text - return self._extract_tags_from_text(response) - - def _extract_tags_from_text(self, text: str) -> List[str]: - """Extract tags from plain text response as fallback. - - Args: - text: Response text - - Returns: - List of extracted tags - """ - # Simple extraction: look for quoted words or comma-separated items - import re - - # Try to find quoted items first - quoted_items = re.findall(r'"([^"]*)"', text) - if quoted_items: - return [ - item.strip().lower() for item in quoted_items if item.strip() - ] - - # Try comma-separated items - lines = text.split("\n") - for line in lines: - if "," in line and not line.startswith("#"): - items = [item.strip().lower() for item in line.split(",")] - if len(items) >= 2: - return [item for item in items if item and len(item) > 1] - - logger.warning("Could not extract tags from response") - return [] - - def extract_tags(self, text: str, title: str = "") -> List[str]: - """Extract tags from arbitrary text. - - Args: - text: Text content to analyze - title: Optional title for context - - Returns: - List of extracted tags - """ - if not self.llm_block: - return [] - - content = f"Title: {title}\n\nContent: {text}" if title else text - return self._generate_tags(content) - - def test_connection(self) -> bool: - """Test if the LLM connection is working. - - Returns: - True if connection is working, False otherwise - """ - if not self.llm_block: - return False - - return self.llm_block.test_connection() - - @property - def llm_type(self) -> str: - """Get the LLM type.""" - return "openai" # Default, can be made configurable if needed - - @property - def llm_name(self) -> str: - """Get the LLM name.""" - return self.config.llm_config.model diff --git a/quantmind/tools/__init__.py b/quantmind/tools/__init__.py deleted file mode 100644 index 46872f1..0000000 --- a/quantmind/tools/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -"""QuantMind tools module. - -This module provides the core tool infrastructure, primarily based on the Smolagents -implementation (@tools.py) as a starting point. It includes base classes, decorators, -and utilities for creating and managing tools within the QuantMind framework. -""" - -from .base import BaseTool, Tool, tool, validate_tool_arguments - -__all__ = [ - "BaseTool", - "Tool", - "tool", - "validate_tool_arguments", -] diff --git a/quantmind/tools/_function_type_hints_utils.py b/quantmind/tools/_function_type_hints_utils.py deleted file mode 100644 index ddb5bad..0000000 --- a/quantmind/tools/_function_type_hints_utils.py +++ /dev/null @@ -1,472 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import inspect -import json -import re -import types -from collections.abc import Callable -from copy import copy -from typing import ( - Any, - Literal, - Union, - get_args, - get_origin, - get_type_hints, -) - -IMPORT_TO_PACKAGE_MAPPING = { - "wikipediaapi": "wikipedia-api", -} - - -def get_package_name(import_name: str) -> str: - """Return the package name for a given import name. - - Args: - import_name (`str`): Import name to get the package name for. - - Returns: - `str`: Package name for the given import name. - """ - return IMPORT_TO_PACKAGE_MAPPING.get(import_name, import_name) - - -def get_imports(code: str) -> list[str]: - """Extracts all the libraries (not relative imports) that are imported in a code. - - Args: - code (`str`): Code text to inspect. - - Returns: - `list[str]`: List of all packages required to use the input code. - """ - # filter out try/except block so in custom code we can have try/except imports - code = re.sub(r"\s*try\s*:.*?except.*?:", "", code, flags=re.DOTALL) - - # filter out imports under is_flash_attn_2_available block for avoid import issues in cpu - # only environment - code = re.sub( - r"if is_flash_attn[a-zA-Z0-9_]+available\(\):\s*(from flash_attn\s*.*\s*)+", - "", - code, - flags=re.MULTILINE, - ) - - # Imports of the form `import xxx` or `import xxx as yyy` - imports = re.findall( - r"^\s*import\s+(\S+?)(?:\s+as\s+\S+)?\s*$", code, flags=re.MULTILINE - ) - # Imports of the form `from xxx import yyy` - imports += re.findall( - r"^\s*from\s+(\S+)\s+import", code, flags=re.MULTILINE - ) - # Only keep the top-level module - imports = [imp.split(".")[0] for imp in imports if not imp.startswith(".")] - return [get_package_name(import_name) for import_name in set(imports)] - - -class TypeHintParsingException(Exception): - """Exception raised for errors in parsing type hints to generate JSON schemas.""" - - -class DocstringParsingException(Exception): - """Exception raised for errors in parsing docstrings to generate JSON schemas.""" - - -def get_json_schema(func: Callable) -> dict: - """Generate a JSON schema for a function based on its docstring and type hints. - - This is mostly used for passing lists of tools to a chat template. The JSON - schema contains the name and description of the function, as well as the - names, types and descriptions for each of its arguments. - `get_json_schema()` requires that the function has a docstring, and that - each argument has a description in the docstring, in the standard Google - docstring format shown below. It also requires that all the function - arguments have a valid Python type hint. - - Although it is not required, a `Returns` block can also be added, which - will be included in the schema. This is optional because most chat - templates ignore the return value of the function. - - Args: - func: The function to generate a JSON schema for. - - Returns: - A dictionary containing the JSON schema for the function. - - Examples: - ```python - >>> def multiply(x: float, y: float): - >>> ''' - >>> A function that multiplies two numbers - >>> - >>> Args: - >>> x: The first number to multiply - >>> y: The second number to multiply - >>> ''' - >>> return x * y - >>> - >>> print(get_json_schema(multiply)) - { - "name": "multiply", - "description": "A function that multiplies two numbers", - "parameters": { - "type": "object", - "properties": { - "x": {"type": "number", "description": "The first number to multiply"}, - "y": {"type": "number", "description": "The second number to multiply"} - }, - "required": ["x", "y"] - } - } - ``` - - The general use for these schemas is that they are used to generate tool descriptions for chat - templates that support them, like so: - - ```python - >>> from transformers import AutoTokenizer - >>> from transformers.utils import get_json_schema - >>> - >>> def multiply(x: float, y: float): - >>> ''' - >>> A function that multiplies two numbers - >>> - >>> Args: - >>> x: The first number to multiply - >>> y: The second number to multiply - >>> return x * y - >>> ''' - >>> - >>> multiply_schema = get_json_schema(multiply) - >>> tokenizer = AutoTokenizer.from_pretrained("CohereForAI/c4ai-command-r-v01") - >>> messages = [{"role": "user", "content": "What is 179 x 4571?"}] - >>> formatted_chat = tokenizer.apply_chat_template( - >>> messages, - >>> tools=[multiply_schema], - >>> chat_template="tool_use", - >>> return_dict=True, - >>> return_tensors="pt", - >>> add_generation_prompt=True - >>> ) - >>> # The formatted chat can now be passed to model.generate() - ``` - - Each argument description can also have an optional `(choices: ...)` block at the end, such as - `(choices: ["tea", "coffee"])`, which will be parsed into an `enum` field in the schema. - Note that this will only be parsed correctly if it is at the end of the line: - - ```python - >>> def drink_beverage(beverage: str): - >>> ''' - >>> A function that drinks a beverage - >>> - >>> Args: - >>> beverage: The beverage to drink (choices: ["tea", "coffee"]) - >>> ''' - >>> pass - >>> - >>> print(get_json_schema(drink_beverage)) - ``` - { - 'name': 'drink_beverage', - 'description': 'A function that drinks a beverage', - 'parameters': { - 'type': 'object', - 'properties': { - 'beverage': { - 'type': 'string', - 'enum': ['tea', 'coffee'], - 'description': 'The beverage to drink' - } - }, - 'required': ['beverage'] - } - } - """ - doc = inspect.getdoc(func) - if not doc: - raise DocstringParsingException( - f"Cannot generate JSON schema for {func.__name__} because it has no docstring!" - ) - doc = doc.strip() - main_doc, param_descriptions, return_doc = _parse_google_format_docstring( - doc - ) - - json_schema = _convert_type_hints_to_json_schema(func) - if ( - return_dict := json_schema["properties"].pop("return", None) - ) is not None: - if ( - return_doc is not None - ): # We allow a missing return docstring since most templates ignore it - return_dict["description"] = return_doc - for arg, schema in json_schema["properties"].items(): - if arg not in param_descriptions: - raise DocstringParsingException( - f"Cannot generate JSON schema for {func.__name__} because the docstring has no description for the argument '{arg}'" - ) - desc = param_descriptions[arg] - enum_choices = re.search( - r"\(choices:\s*(.*?)\)\s*$", desc, flags=re.IGNORECASE - ) - if enum_choices: - schema["enum"] = [ - c.strip() for c in json.loads(enum_choices.group(1)) - ] - desc = enum_choices.string[: enum_choices.start()].strip() - schema["description"] = desc - - output = { - "name": func.__name__, - "description": main_doc, - "parameters": json_schema, - } - if return_dict is not None: - output["return"] = return_dict - return {"type": "function", "function": output} - - -# Extracts the initial segment of the docstring, containing the function description -description_re = re.compile( - r"^(.*?)(?=\n\s*(Args:|Returns:|Raises:)|\Z)", re.DOTALL -) -# Extracts the Args: block from the docstring -args_re = re.compile( - r"\n\s*Args:\n\s*(.*?)[\n\s]*(Returns:|Raises:|\Z)", re.DOTALL -) -# Splits the Args: block into individual arguments -args_split_re = re.compile( - r"(?:^|\n)" # Match the start of the args block, or a newline - r"\s*(\w+)\s*(?:\([^)]*?\))?:\s*" # Capture the argument name (ignore the type) and strip spacing - r"(.*?)\s*" # Capture the argument description, which can span multiple lines, and strip trailing spacing - r"(?=\n\s*\w+\s*(?:\([^)]*?\))?:|\Z)", # Stop when you hit the next argument (with or without type) or the end of the block - re.DOTALL | re.VERBOSE, -) -# Extracts the Returns: block from the docstring, if present. Note that most chat templates ignore -# the return type/doc! -returns_re = re.compile( - r"\n\s*Returns:\n\s*" - r"(?:[^)]*?:\s*)?" # Ignore the return type if present - r"(.*?)" # Capture the return description - r"[\n\s]*(Raises:|\Z)", - re.DOTALL, -) - - -def _parse_google_format_docstring( - docstring: str, -) -> tuple[str | None, dict | None, str | None]: - """Parses a Google-style docstring. - - Parses a Google-style docstring to extract the function description, argument descriptions, - and return description. - - Args: - docstring (str): The docstring to parse. - - Returns: - The function description, arguments, and return description. - """ - # Extract the sections - description_match = description_re.search(docstring) - args_match = args_re.search(docstring) - returns_match = returns_re.search(docstring) - - # Clean and store the sections - description = ( - description_match.group(1).strip() if description_match else None - ) - docstring_args = args_match.group(1).strip() if args_match else None - returns = returns_match.group(1).strip() if returns_match else None - - # Parsing the arguments into a dictionary - if docstring_args is not None: - docstring_args = "\n".join( - [line for line in docstring_args.split("\n") if line.strip()] - ) # Remove blank lines - matches = args_split_re.findall(docstring_args) - args_dict = { - match[0]: re.sub(r"\s*\n+\s*", " ", match[1].strip()) - for match in matches - } - else: - args_dict = {} - - return description, args_dict, returns - - -def _convert_type_hints_to_json_schema( - func: Callable, error_on_missing_type_hints: bool = True -) -> dict: - type_hints = get_type_hints(func) - signature = inspect.signature(func) - - properties = {} - for param_name, param_type in type_hints.items(): - properties[param_name] = _parse_type_hint(param_type) - - required = [] - for param_name, param in signature.parameters.items(): - if ( - param.annotation == inspect.Parameter.empty - and error_on_missing_type_hints - ): - raise TypeHintParsingException( - f"Argument {param.name} is missing a type hint in function {func.__name__}" - ) - if param_name not in properties: - properties[param_name] = {} - - if param.default == inspect.Parameter.empty: - required.append(param_name) - else: - properties[param_name]["nullable"] = True - - # Return: multi‐type union -> treat as any - if ( - "return" in properties - and (return_type := properties["return"].get("type")) - and not isinstance(return_type, str) - ): - properties["return"]["type"] = "any" - - schema = {"type": "object", "properties": properties} - if required: - schema["required"] = required - - return schema - - -def _parse_type_hint(hint: type) -> dict: - origin = get_origin(hint) - args = get_args(hint) - - if origin is None: - try: - return _get_json_schema_type(hint) - except KeyError: - raise TypeHintParsingException( - "Couldn't parse this type hint, likely due to a custom class or object: ", - hint, - ) - - elif origin is Union or ( - hasattr(types, "UnionType") and origin is types.UnionType - ): - return _parse_union_type(args) - - elif origin is list: - if not args: - return {"type": "array"} - else: - # Lists can only have a single type argument, so recurse into it - return {"type": "array", "items": _parse_type_hint(args[0])} - - elif origin is tuple: - if not args: - return {"type": "array"} - if len(args) == 1: - raise TypeHintParsingException( - f"The type hint {str(hint).replace('typing.', '')} is a Tuple with a single element, which " - "we do not automatically convert to JSON schema as it is rarely necessary. If this input can contain " - "more than one element, we recommend " - "using a List[] type instead, or if it really is a single element, remove the Tuple[] wrapper and just " - "pass the element directly." - ) - if ... in args: - raise TypeHintParsingException( - "Conversion of '...' is not supported in Tuple type hints. " - "Use List[] types for variable-length" - " inputs instead." - ) - return { - "type": "array", - "prefixItems": [_parse_type_hint(t) for t in args], - } - - elif origin is dict: - # The JSON equivalent to a dict is 'object', which mandates that all keys are strings - # However, we can specify the type of the dict values with "additionalProperties" - out = {"type": "object"} - if len(args) == 2: - out["additionalProperties"] = _parse_type_hint(args[1]) - return out - - elif origin is Literal: - literal_types = set(type(arg) for arg in args) - final_type = _parse_union_type(literal_types) - - # None literal value is represented by 'nullable' field set by _parse_union_type - final_type.update({"enum": [arg for arg in args if arg is not None]}) - return final_type - - raise TypeHintParsingException( - "Couldn't parse this type hint, likely due to a custom class or object: ", - hint, - ) - - -def _parse_union_type(args: tuple[Any, ...]) -> dict: - subtypes = [_parse_type_hint(t) for t in args if t is not type(None)] - if len(subtypes) == 1: - # A single non-null type can be expressed directly - return_dict = subtypes[0] - elif all(isinstance(subtype["type"], str) for subtype in subtypes): - # A union of basic types can be expressed as a list in the schema - return_dict = { - "type": sorted([subtype["type"] for subtype in subtypes]) - } - else: - # A union of more complex types requires "anyOf" - return_dict = {"anyOf": subtypes} - if type(None) in args: - return_dict["nullable"] = True - return return_dict - - -_BASE_TYPE_MAPPING = { - int: {"type": "integer"}, - float: {"type": "number"}, - str: {"type": "string"}, - bool: {"type": "boolean"}, - list: {"type": "array"}, - dict: {"type": "object"}, - Any: {"type": "any"}, - types.NoneType: {"type": "null"}, -} - - -def _get_json_schema_type(param_type: type) -> dict[str, str]: - if param_type in _BASE_TYPE_MAPPING: - return copy(_BASE_TYPE_MAPPING[param_type]) - if str(param_type) == "Image": - from PIL.Image import Image - - if param_type == Image: - return {"type": "image"} - if str(param_type) == "Tensor": - try: - from torch import Tensor - - if param_type == Tensor: - return {"type": "audio"} - except ModuleNotFoundError: - pass - return {"type": "object"} diff --git a/quantmind/tools/_tool_validation.py b/quantmind/tools/_tool_validation.py deleted file mode 100644 index 368a4a1..0000000 --- a/quantmind/tools/_tool_validation.py +++ /dev/null @@ -1,300 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import ast -import builtins -from itertools import zip_longest - -from .utils import BASE_BUILTIN_MODULES, get_source, is_valid_name - -_BUILTIN_NAMES = set(vars(builtins)) - - -class MethodChecker(ast.NodeVisitor): - """Checks that a method. - - - only uses defined names - - contains no local imports (e.g. numpy is ok but local_script is not) - """ - - def __init__(self, class_attributes: set[str], check_imports: bool = True): - self.undefined_names = set() - self.imports = {} - self.from_imports = {} - self.assigned_names = set() - self.arg_names = set() - self.class_attributes = class_attributes - self.errors = [] - self.check_imports = check_imports - self.typing_names = {"Any"} - self.defined_classes = set() - - def visit_arguments(self, node): - """Collect function arguments.""" - self.arg_names = {arg.arg for arg in node.args} - if node.kwarg: - self.arg_names.add(node.kwarg.arg) - if node.vararg: - self.arg_names.add(node.vararg.arg) - - def visit_Import(self, node): - for name in node.names: - actual_name = name.asname or name.name - self.imports[actual_name] = name.name - - def visit_ImportFrom(self, node): - module = node.module or "" - for name in node.names: - actual_name = name.asname or name.name - self.from_imports[actual_name] = (module, name.name) - - def visit_Assign(self, node): - for target in node.targets: - if isinstance(target, ast.Name): - self.assigned_names.add(target.id) - elif isinstance(target, (ast.Tuple, ast.List)): - for elt in target.elts: - if isinstance(elt, ast.Name): - self.assigned_names.add(elt.id) - self.visit(node.value) - - def visit_With(self, node): - """Track aliases in 'with' statements (the 'y' in 'with X as y').""" - for item in node.items: - if item.optional_vars: # This is the 'y' in 'with X as y' - if isinstance(item.optional_vars, ast.Name): - self.assigned_names.add(item.optional_vars.id) - self.generic_visit(node) - - def visit_ExceptHandler(self, node): - """Track exception aliases (the 'e' in 'except Exception as e').""" - if node.name: # This is the 'e' in 'except Exception as e' - self.assigned_names.add(node.name) - self.generic_visit(node) - - def visit_AnnAssign(self, node): - """Track annotated assignments.""" - if isinstance(node.target, ast.Name): - self.assigned_names.add(node.target.id) - if node.value: - self.visit(node.value) - - def visit_For(self, node): - target = node.target - if isinstance(target, ast.Name): - self.assigned_names.add(target.id) - elif isinstance(target, ast.Tuple): - for elt in target.elts: - if isinstance(elt, ast.Name): - self.assigned_names.add(elt.id) - self.generic_visit(node) - - def _handle_comprehension_generators(self, generators): - """Helper method to handle generators in all types of comprehensions.""" - for generator in generators: - if isinstance(generator.target, ast.Name): - self.assigned_names.add(generator.target.id) - elif isinstance(generator.target, ast.Tuple): - for elt in generator.target.elts: - if isinstance(elt, ast.Name): - self.assigned_names.add(elt.id) - - def visit_ListComp(self, node): - """Track variables in list comprehensions.""" - self._handle_comprehension_generators(node.generators) - self.generic_visit(node) - - def visit_DictComp(self, node): - """Track variables in dictionary comprehensions.""" - self._handle_comprehension_generators(node.generators) - self.generic_visit(node) - - def visit_SetComp(self, node): - """Track variables in set comprehensions.""" - self._handle_comprehension_generators(node.generators) - self.generic_visit(node) - - def visit_Attribute(self, node): - if not (isinstance(node.value, ast.Name) and node.value.id == "self"): - self.generic_visit(node) - - def visit_ClassDef(self, node): - """Track class definitions.""" - self.defined_classes.add(node.name) - self.generic_visit(node) - - def visit_Name(self, node): - if isinstance(node.ctx, ast.Load): - if not ( - node.id in _BUILTIN_NAMES - or node.id in BASE_BUILTIN_MODULES - or node.id in self.arg_names - or node.id == "self" - or node.id in self.class_attributes - or node.id in self.imports - or node.id in self.from_imports - or node.id in self.assigned_names - or node.id in self.typing_names - or node.id in self.defined_classes - ): - self.errors.append(f"Name '{node.id}' is undefined.") - - def visit_Call(self, node): - if isinstance(node.func, ast.Name): - if not ( - node.func.id in _BUILTIN_NAMES - or node.func.id in BASE_BUILTIN_MODULES - or node.func.id in self.arg_names - or node.func.id == "self" - or node.func.id in self.class_attributes - or node.func.id in self.imports - or node.func.id in self.from_imports - or node.func.id in self.assigned_names - or node.func.id in self.defined_classes - ): - self.errors.append(f"Name '{node.func.id}' is undefined.") - self.generic_visit(node) - - -def validate_tool_attributes(cls, check_imports: bool = True) -> None: - """Validates that a Tool class follows the proper patterns. - - 0. Any argument of __init__ should have a default. - Args chosen at init are not traceable, so we cannot rebuild the source - code for them, thus any important arg should be defined as a class - attribute. - 1. About the class: - - Class attributes should only be strings or dicts - - Class attributes cannot be complex attributes - 2. About all class methods: - - Imports must be from packages, not local files - - All methods must be self-contained - - Raises all errors encountered, if no error returns None. - """ - - class ClassLevelChecker(ast.NodeVisitor): - def __init__(self): - self.imported_names = set() - self.complex_attributes = set() - self.class_attributes = set() - self.non_defaults = set() - self.non_literal_defaults = set() - self.in_method = False - self.invalid_attributes = [] - - def visit_FunctionDef(self, node): - if node.name == "__init__": - self._check_init_function_parameters(node) - old_context = self.in_method - self.in_method = True - self.generic_visit(node) - self.in_method = old_context - - def visit_Assign(self, node): - if self.in_method: - return - # Track class attributes - for target in node.targets: - if isinstance(target, ast.Name): - self.class_attributes.add(target.id) - - # Check if the assignment is more complex than simple literals - if not all( - isinstance(val, (ast.Constant, ast.Dict, ast.List, ast.Set)) - for val in ast.walk(node.value) - ): - for target in node.targets: - if isinstance(target, ast.Name): - self.complex_attributes.add(target.id) - - # Check specific class attributes - if getattr(node.targets[0], "id", "") == "name": - if not isinstance(node.value, ast.Constant): - self.invalid_attributes.append( - f"Class attribute 'name' must be a constant, found '{node.value}'" - ) - elif not isinstance(node.value.value, str): - self.invalid_attributes.append( - f"Class attribute 'name' must be a string, found '{node.value.value}'" - ) - elif not is_valid_name(node.value.value): - self.invalid_attributes.append( - f"Class attribute 'name' must be a valid Python identifier and not a reserved keyword, found '{node.value.value}'" - ) - - def _check_init_function_parameters(self, node): - # Check defaults in parameters - for arg, default in reversed( - list( - zip_longest( - reversed(node.args.args), reversed(node.args.defaults) - ) - ) - ): - if default is None: - if arg.arg != "self": - self.non_defaults.add(arg.arg) - elif not isinstance( - default, (ast.Constant, ast.Dict, ast.List, ast.Set) - ): - self.non_literal_defaults.add(arg.arg) - - class_level_checker = ClassLevelChecker() - source = get_source(cls) - tree = ast.parse(source) - class_node = tree.body[0] - if not isinstance(class_node, ast.ClassDef): - raise ValueError("Source code must define a class") - class_level_checker.visit(class_node) - - errors = [] - # Check invalid class attributes - if class_level_checker.invalid_attributes: - errors += class_level_checker.invalid_attributes - if class_level_checker.complex_attributes: - errors.append( - f"Complex attributes should be defined in __init__, not as class attributes: " - f"{', '.join(class_level_checker.complex_attributes)}" - ) - if class_level_checker.non_defaults: - errors.append( - f"Parameters in __init__ must have default values, found required parameters: " - f"{', '.join(class_level_checker.non_defaults)}" - ) - if class_level_checker.non_literal_defaults: - errors.append( - f"Parameters in __init__ must have literal default values, found non-literal defaults: " - f"{', '.join(class_level_checker.non_literal_defaults)}" - ) - - # Run checks on all methods - for node in class_node.body: - if isinstance(node, ast.FunctionDef): - method_checker = MethodChecker( - class_level_checker.class_attributes, - check_imports=check_imports, - ) - method_checker.visit(node) - errors += [ - f"- {node.name}: {error}" for error in method_checker.errors - ] - - if errors: - raise ValueError( - f"Tool validation failed for {cls.__name__}:\n" + "\n".join(errors) - ) - return diff --git a/quantmind/tools/base.py b/quantmind/tools/base.py deleted file mode 100644 index d702d3a..0000000 --- a/quantmind/tools/base.py +++ /dev/null @@ -1,715 +0,0 @@ -"""Tool system implementation for QuantMind. - -This module provides the core tool infrastructure, primarily based on the Smolagents -implementation (@tools.py) as a starting point. It includes base classes, decorators, -and utilities for creating and managing tools within the QuantMind framework. -""" - -from __future__ import annotations - -import ast -import inspect -import json -import logging -import sys -import textwrap -import types -import warnings -from abc import ABC, abstractmethod -from collections.abc import Callable -from functools import wraps -from pathlib import Path -from typing import Any - -from ._function_type_hints_utils import ( - TypeHintParsingException, - _get_json_schema_type, - get_imports, - get_json_schema, -) -from ._tool_validation import MethodChecker, validate_tool_attributes -from .utils import ( - BASE_BUILTIN_MODULES, - get_source, - instance_to_source, - is_valid_name, -) - -logger = logging.getLogger(__name__) - - -def validate_after_init(cls): - """A class decorator that automatically validates tool arguments after initialization. - - This decorator wraps the class's __init__ method to call validate_arguments() - immediately after the original initialization is complete. This ensures that - any tool instance is validated upon creation without requiring manual validation calls. - - Args: - cls: The class to be decorated (typically a Tool subclass) - - Returns: - The decorated class with automatic post-init validation - """ - original_init = cls.__init__ - - @wraps(original_init) - def new_init(self, *args, **kwargs): - original_init(self, *args, **kwargs) - self.validate_arguments() - - cls.__init__ = new_init - return cls - - -AUTHORIZED_TYPES = [ - "string", - "boolean", - "integer", - "number", - "image", - "audio", - "array", - "object", - "any", - "null", -] - -CONVERSION_DICT = {"str": "string", "int": "integer", "float": "number"} - - -class BaseTool(ABC): - name: str - - @abstractmethod - def __call__(self, *args, **kwargs) -> Any: - pass - - -class Tool(BaseTool): - """A base class for the functions used by the agent. - - Subclass this and implement the `forward` method as well as the following - class attributes. - - Attributes: - description (str): A short description of what your tool does, the - inputs it expects and the output(s) it will return. For instance - 'This is a tool that downloads a file from a `url`. It takes the - `url` as input, and returns the text contained in the file'. - name (str): A performative name that will be used for your tool in - the prompt to the agent. For instance `"text-classifier"` or - `"image_generator"`. - inputs (Dict[str, Dict[str, Union[str, type, bool]]]): The dict of - modalities expected for the inputs. It has one `type` key and a - `description` key. This is used by `launch_gradio_demo` or to make - a nice space from your tool, and also can be used in the generated - description for your tool. - output_type (type): The type of the tool output. This is used by - `launch_gradio_demo` or to make a nice space from your tool, and - also can be used in the generated description for your tool. - output_schema (Dict[str, Any], optional): The JSON schema defining the - expected structure of the tool output. This can be included in - system prompts to help agents understand the expected output - format. Note: This is currently used for informational purposes - only and does not perform actual output validation. - - Note: - You can also override the method [`~Tool.setup`] if your tool has an - expensive operation to perform before being usable (such as loading a - model). [`~Tool.setup`] will be called the first time you use your - tool, but not at instantiation. - """ - - name: str - description: str - inputs: dict[str, dict[str, str | type | bool]] - output_type: str - output_schema: dict[str, Any] | None = None - - def __init__(self, *args, **kwargs): - self.is_initialized = False - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - validate_after_init(cls) - - def validate_arguments(self): - """Validate the tool's arguments. - - This method validates the tool's arguments to ensure they are of the - correct type and format. - """ - required_attributes = { - "description": str, - "name": str, - "inputs": dict, - "output_type": str, - } - # Validate class attributes - for attr, expected_type in required_attributes.items(): - attr_value = getattr(self, attr, None) - if attr_value is None: - raise TypeError(f"You must set an attribute {attr}.") - if not isinstance(attr_value, expected_type): - raise TypeError( - f"Attribute {attr} should have type {expected_type.__name__}, got {type(attr_value)} instead." - ) - - # Validate optional output_schema attribute - output_schema = getattr(self, "output_schema", None) - if output_schema is not None and not isinstance(output_schema, dict): - raise TypeError( - f"Attribute output_schema should have type dict, got {type(output_schema)} instead." - ) - - # - Validate name - if not is_valid_name(self.name): - raise Exception( - f"Invalid Tool name '{self.name}': must be a valid Python identifier and not a reserved keyword" - ) - - # Validate inputs - for input_name, input_content in self.inputs.items(): - assert isinstance( - input_content, dict - ), f"Input '{input_name}' should be a dictionary." - assert ( - "type" in input_content and "description" in input_content - ), f"Input '{input_name}' should have keys 'type' and 'description', has only {list(input_content.keys())}." - # Get input_types as a list, whether from a string or list - if isinstance(input_content["type"], str): - input_types = [input_content["type"]] - elif isinstance(input_content["type"], list): - input_types = input_content["type"] - # Check if all elements are strings - if not all(isinstance(t, str) for t in input_types): - raise TypeError( - f"Input '{input_name}': when type is a list, all elements must be strings, got {input_content['type']}" - ) - else: - raise TypeError( - f"Input '{input_name}': type must be a string or list of strings, got {type(input_content['type']).__name__}" - ) - # Check all types are authorized - invalid_types = [ - t for t in input_types if t not in AUTHORIZED_TYPES - ] - if invalid_types: - raise ValueError( - f"Input '{input_name}': types {invalid_types} must be one of {AUTHORIZED_TYPES}" - ) - # Validate output type - assert getattr(self, "output_type", None) in AUTHORIZED_TYPES - - def forward(self, *args, **kwargs): - """Implement the forward method in your subclass of `Tool`.""" - raise NotImplementedError( - "Write this method in your subclass of `Tool`." - ) - - def __call__(self, *args, **kwargs): - """Call the tool. - - This method calls the tool's forward method with the given arguments. - """ - if not self.is_initialized: - self.setup() - - # Handle the arguments might be passed as a single dictionary - if len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], dict): - potential_kwargs = args[0] - - # If the dictionary keys match our input parameters, convert it to kwargs - if all(key in self.inputs for key in potential_kwargs): - args = () - kwargs = potential_kwargs - - outputs = self.forward(*args, **kwargs) - return outputs - - def setup(self): - """Setup the tool. - - Overwrite this method here for any operation that is expensive and needs to be executed - before you start using your tool. Such as loading a big model. - """ - self.is_initialized = True - - def to_code_prompt(self) -> str: - """TODO: Add docstring & example for `to_code_prompt` function.""" - args_signature = ", ".join( - f"{arg_name}: {arg_schema['type']}" - for arg_name, arg_schema in self.inputs.items() - ) - - # Use dict type for tools with output schema to indicate structured return - has_schema = ( - hasattr(self, "output_schema") and self.output_schema is not None - ) - output_type = "dict" if has_schema else self.output_type - tool_signature = f"({args_signature}) -> {output_type}" - tool_doc = self.description - - # Add an important note for smaller models (e.g. Mistral Small, Gemma 3, etc.) - # to properly handle structured output. - if has_schema: - IMPORTANT_NOTE_FOR_SMALL_MODELS = "Important: This tool returns structured output! Use the JSON schema below to directly access fields like result['field_name']. NO print() statements needed to inspect the output!" - tool_doc += "\n\n" + IMPORTANT_NOTE_FOR_SMALL_MODELS - - # Add arguments documentation - if self.inputs: - args_descriptions = "\n".join( - f"{arg_name}: {arg_schema['description']}" - for arg_name, arg_schema in self.inputs.items() - ) - args_doc = f"Args:\n{textwrap.indent(args_descriptions, ' ')}" - tool_doc += f"\n\n{args_doc}" - - # Add returns documentation with output schema if it exists - if has_schema: - formatted_schema = json.dumps(self.output_schema, indent=4) - indented_schema = textwrap.indent(formatted_schema, " ") - returns_doc = f"\nReturns:\n dict (structured output): This tool ALWAYS returns a dictionary that strictly adheres to the following JSON schema:\n{indented_schema}" - tool_doc += f"\n{returns_doc}" - - tool_doc = f'"""{tool_doc}\n"""' - return f"def {self.name}{tool_signature}:\n{textwrap.indent(tool_doc, ' ')}" - - def to_tool_calling_prompt(self) -> str: - return f"{self.name}: {self.description}\n Takes inputs: {self.inputs}\n Returns an output of type: {self.output_type}" - - def to_dict(self) -> dict: - """Returns a dictionary representing the tool. - - Note: Inherit from Smolagents impl. - """ - class_name = self.__class__.__name__ - if type(self).__name__ == "SimpleTool": - # Check that imports are self-contained - source_code = get_source(self.forward).replace("@tool", "") - forward_node = ast.parse(source_code) - # If tool was created using '@tool' decorator, - # it has only a forward pass, so it's simpler to just get its code - method_checker = MethodChecker(set()) - method_checker.visit(forward_node) - - if len(method_checker.errors) > 0: - errors = [f"- {error}" for error in method_checker.errors] - raise ( - ValueError( - f"SimpleTool validation failed for {self.name}:\n" - + "\n".join(errors) - ) - ) - - forward_source_code = get_source(self.forward) - tool_code = textwrap.dedent( - f""" - from quantmind import Tool - from typing import Any, Optional - - class {class_name}(Tool): - name = "{self.name}" - description = {json.dumps(textwrap.dedent(self.description).strip())} - inputs = {repr(self.inputs)} - output_type = "{self.output_type}" - """ - ).strip() - - # Add output_schema if it exists - if ( - hasattr(self, "output_schema") - and self.output_schema is not None - ): - tool_code += f"\n output_schema = {repr(self.output_schema)}" - import re - - def add_self_argument(source_code: str) -> str: - """Add 'self' as first argument to a function definition if not present.""" - pattern = r"def forward\(((?!self)[^)]*)\)" - - def replacement(match): - args = match.group(1).strip() - if args: # If there are other arguments - return f"def forward(self, {args})" - return "def forward(self)" - - return re.sub(pattern, replacement, source_code) - - forward_source_code = forward_source_code.replace( - self.name, "forward" - ) - forward_source_code = add_self_argument(forward_source_code) - forward_source_code = forward_source_code.replace( - "@tool", "" - ).strip() - tool_code += "\n\n" + textwrap.indent(forward_source_code, " ") - - else: # If the tool was not created by the @tool decorator, it was made by subclassing Tool - validate_tool_attributes(self.__class__) - - tool_code = ( - "from typing import Any, Optional\n" - + instance_to_source(self, base_cls=Tool) - ) - - requirements = { - el - for el in get_imports(tool_code) - if el not in sys.stdlib_module_names - } | {"quantmind"} - - tool_dict = { - "name": self.name, - "code": tool_code, - "requirements": sorted(requirements), - } - - # Add output_schema if it exists - if hasattr(self, "output_schema") and self.output_schema is not None: - tool_dict["output_schema"] = self.output_schema - - return tool_dict - - @classmethod - def from_dict(cls, tool_dict: dict[str, Any], **kwargs) -> "Tool": - """Create tool from a dictionary representation. - - Args: - tool_dict (`dict[str, Any]`): Dictionary representation of the tool. - **kwargs: Additional keyword arguments to pass to the tool's constructor. - - Returns: - `Tool`: Tool object. - """ - if "code" not in tool_dict: - raise ValueError( - "Tool dictionary must contain 'code' key with the tool source code" - ) - - tool = cls.from_code(tool_dict["code"], **kwargs) - - # Set output_schema if it exists in the dictionary - if "output_schema" in tool_dict: - tool.output_schema = tool_dict["output_schema"] - - return tool - - def save(self, output_dir: str | Path, tool_file_name: str = "tool"): - """Saves the relevant code files for your tool. - - This will copy the code of your tool in `output_dir` as well as autogenerate: - - - a `{tool_file_name}.py` file containing the logic for your tool. - - Args: - output_dir (`str` or `Path`): The folder in which you want to save your tool. - tool_file_name (`str`, *optional*): The file name in which you want to save your tool. - """ - # Ensure output directory exists - output_path = Path(output_dir) - output_path.mkdir(parents=True, exist_ok=True) - # Save tool file - self._write_file( - output_path / f"{tool_file_name}.py", self._get_tool_code() - ) - - def _write_file(self, file_path: Path, content: str) -> None: - """Writes content to a file with UTF-8 encoding.""" - file_path.write_text(content, encoding="utf-8") - - def _get_tool_code(self) -> str: - """Get the tool's code.""" - return self.to_dict()["code"] - - def _get_requirements(self) -> str: - """Get the requirements.""" - return "\n".join(self.to_dict()["requirements"]) - - @classmethod - def from_code(cls, tool_code: str, **kwargs): - module = types.ModuleType("dynamic_tool") - - exec(tool_code, module.__dict__) - - # Find the Tool subclass - tool_class = next( - ( - obj - for _, obj in inspect.getmembers(module, inspect.isclass) - if issubclass(obj, Tool) and obj is not Tool - ), - None, - ) - - if tool_class is None: - raise ValueError("No Tool subclass found in the code.") - - # Convert inputs from string representation to dictionary if needed - # When tool code is serialized/deserialized, complex data structures like - # dictionaries may be stored as string literals (e.g., "{'key': 'value'}") - # ast.literal_eval safely evaluates these string literals back to Python objects - if not isinstance(tool_class.inputs, dict): - tool_class.inputs = ast.literal_eval(tool_class.inputs) - - # Handle output_schema if it exists and is a string representation - # Similar to inputs, output_schema might be serialized as a string literal - # and needs to be converted back to its original Python data structure - if hasattr(tool_class, "output_schema") and isinstance( - tool_class.output_schema, str - ): - # ast.literal_eval is safer than eval() as it only evaluates literals - # (strings, numbers, tuples, lists, dicts, booleans, None) - # and prevents execution of arbitrary code - tool_class.output_schema = ast.literal_eval( - tool_class.output_schema - ) - - return tool_class(**kwargs) - - -def add_description(description): - """A decorator that adds a description to a function.""" - - def inner(func): - func.description = description - func.name = func.__name__ - return func - - return inner - - -def tool(tool_function: Callable) -> Tool: - """Convert a function into an instance of a dynamically created Tool subclass. - - Args: - tool_function (`Callable`): Function to convert into a Tool subclass. - Should have type hints for each input and a type hint for the output. - Should also have a docstring including the description of the function - and an 'Args:' part where each argument is described. - """ - tool_json_schema = get_json_schema(tool_function)["function"] - if "return" not in tool_json_schema: - if len(tool_json_schema["parameters"]["properties"]) == 0: - tool_json_schema["return"] = {"type": "null"} - else: - raise TypeHintParsingException( - "Tool return type not found: make sure your function has a return type hint!" - ) - - class SimpleTool(Tool): - def __init__(self): - self.is_initialized = True - - # Set the class attributes - SimpleTool.name = tool_json_schema["name"] - SimpleTool.description = tool_json_schema["description"] - SimpleTool.inputs = tool_json_schema["parameters"]["properties"] - SimpleTool.output_type = tool_json_schema["return"]["type"] - - # Set output_schema if it exists in the JSON schema - if "output_schema" in tool_json_schema: - SimpleTool.output_schema = tool_json_schema["output_schema"] - elif ( - "return" in tool_json_schema and "schema" in tool_json_schema["return"] - ): - SimpleTool.output_schema = tool_json_schema["return"]["schema"] - - @wraps(tool_function) - def wrapped_function(*args, **kwargs): - return tool_function(*args, **kwargs) - - # Bind the copied function to the forward method - SimpleTool.forward = staticmethod(wrapped_function) - - # Get the signature parameters of the tool function - sig = inspect.signature(tool_function) - # - Add "self" as first parameter to tool_function signature - new_sig = sig.replace( - parameters=[ - inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD) - ] - + list(sig.parameters.values()) - ) - # - Set the signature of the forward method - SimpleTool.forward.__signature__ = new_sig - - # Create and attach the source code of the dynamically created tool class and forward method - # - Get the source code of tool_function - tool_source = textwrap.dedent(inspect.getsource(tool_function)) - # - Remove the tool decorator and function definition line - lines = tool_source.splitlines() - tree = ast.parse(tool_source) - # - Find function definition - func_node = next( - (node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)), - None, - ) - if not func_node: - raise ValueError( - f"No function definition found in the provided source of {tool_function.__name__}. " - "Ensure the input is a standard function." - ) - # - Extract decorator lines - decorator_lines = "" - if func_node.decorator_list: - tool_decorators = [ - d - for d in func_node.decorator_list - if isinstance(d, ast.Name) and d.id == "tool" - ] - if len(tool_decorators) > 1: - raise ValueError( - f"Multiple @tool decorators found on function '{func_node.name}'. Only one @tool decorator is allowed." - ) - if len(tool_decorators) < len(func_node.decorator_list): - warnings.warn( - f"Function '{func_node.name}' has decorators other than @tool. " - "This may cause issues with serialization in the remote executor. See issue #1626." - ) - decorator_start = ( - tool_decorators[0].end_lineno if tool_decorators else 0 - ) - decorator_end = func_node.decorator_list[-1].end_lineno - decorator_lines = "\n".join(lines[decorator_start:decorator_end]) - # - Extract tool source body - body_start = func_node.body[0].lineno - 1 # AST lineno starts at 1 - tool_source_body = "\n".join(lines[body_start:]) - # - Create the forward method source, including def line and indentation - forward_method_source = f"def forward{new_sig}:\n{tool_source_body}" - # - Create the class source - indent = " " * 4 # for class method - class_source = ( - textwrap.dedent(f""" - class SimpleTool(Tool): - name: str = "{tool_json_schema["name"]}" - description: str = {json.dumps(textwrap.dedent(tool_json_schema["description"]).strip())} - inputs: dict[str, dict[str, str]] = {tool_json_schema["parameters"]["properties"]} - output_type: str = "{tool_json_schema["return"]["type"]}" - - def __init__(self): - self.is_initialized = True - - """) - + textwrap.indent(decorator_lines, indent) - + textwrap.indent(forward_method_source, indent) - ) - # - Store the source code on both class and method for inspection - SimpleTool.__source__ = class_source - SimpleTool.forward.__source__ = forward_method_source - - simple_tool = SimpleTool() - return simple_tool - - -def get_tools_definition_code(tools: dict[str, Tool]) -> str: - """Get the tools definition code. - - This function gets the tools definition code. - - Args: - tools (`dict[str, Tool]`): The tools to get the definition code for. - - Returns: - `str`: The tools definition code. - """ - tool_codes = [] - for tool in tools.values(): - validate_tool_attributes(tool.__class__, check_imports=False) - tool_code = instance_to_source(tool, base_cls=Tool) - tool_code = tool_code.replace("from quantmind.tools import Tool", "") - tool_code += f"\n\n{tool.name} = {tool.__class__.__name__}()\n" - tool_codes.append(tool_code) - - tool_definition_code = "\n".join( - [f"import {module}" for module in BASE_BUILTIN_MODULES] - ) - tool_definition_code += textwrap.dedent( - """ - from typing import Any - - class Tool: - def __call__(self, *args, **kwargs): - return self.forward(*args, **kwargs) - - def forward(self, *args, **kwargs): - pass # to be implemented in child class - """ - ) - tool_definition_code += "\n\n".join(tool_codes) - return tool_definition_code - - -def validate_tool_arguments(tool: Tool, arguments: Any) -> None: - """Validate tool arguments against tool's input schema. - - Checks that all provided arguments match the tool's expected input types and that - all required arguments are present. Supports both dictionary arguments and single - value arguments for tools with one input parameter. - - Args: - tool (`Tool`): Tool whose input schema will be used for validation. - arguments (`Any`): Arguments to validate. Can be a dictionary mapping - argument names to values, or a single value for tools with one input. - - - Raises: - ValueError: If an argument is not in the tool's input schema, if a required - argument is missing, or if the argument value doesn't match the expected type. - TypeError: If an argument has an incorrect type that cannot be converted - (e.g., string instead of number, excluding integer to number conversion). - - Note: - - Supports type coercion from integer to number - - Handles nullable parameters when explicitly marked in the schema - - Accepts "any" type as a wildcard that matches all types - """ - if isinstance(arguments, dict): - for key, value in arguments.items(): - if key not in tool.inputs: - raise ValueError( - f"Argument {key} is not in the tool's input schema" - ) - - actual_type = _get_json_schema_type(type(value))["type"] - expected_type = tool.inputs[key]["type"] - expected_type_is_nullable = tool.inputs[key].get("nullable", False) - - # Type is valid if it matches, is "any", or is null for nullable parameters - if ( - ( - actual_type != expected_type - if isinstance(expected_type, str) - else actual_type not in expected_type - ) - and expected_type != "any" - and not (actual_type == "null" and expected_type_is_nullable) - ): - if actual_type == "integer" and expected_type == "number": - continue - raise TypeError( - f"Argument {key} has type '{actual_type}' but should be '{tool.inputs[key]['type']}'" - ) - - for key, schema in tool.inputs.items(): - key_is_nullable = schema.get("nullable", False) - if key not in arguments and not key_is_nullable: - raise ValueError(f"Argument {key} is required") - return None - else: - expected_type = list(tool.inputs.values())[0]["type"] - if ( - _get_json_schema_type(type(arguments))["type"] != expected_type - and not expected_type == "any" - ): - raise TypeError( - f"Argument has type '{type(arguments).__name__}' but should be '{expected_type}'" - ) - - -__all__ = [ - "AUTHORIZED_TYPES", - "Tool", - "tool", -] diff --git a/quantmind/tools/utils.py b/quantmind/tools/utils.py deleted file mode 100644 index 16056d5..0000000 --- a/quantmind/tools/utils.py +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import ast -import inspect -import json -import keyword -from textwrap import dedent - -BASE_BUILTIN_MODULES = [ - "collections", - "datetime", - "itertools", - "math", - "queue", - "random", - "re", - "stat", - "statistics", - "time", - "unicodedata", -] - - -class ImportFinder(ast.NodeVisitor): - """Finds the packages imported in a code.""" - - def __init__(self): - self.packages = set() - - def visit_Import(self, node): - for alias in node.names: - # Get the base package name (before any dots) - base_package = alias.name.split(".")[0] - self.packages.add(base_package) - - def visit_ImportFrom(self, node): - if node.module: # for "from x import y" statements - # Get the base package name (before any dots) - base_package = node.module.split(".")[0] - self.packages.add(base_package) - - -def instance_to_source(instance, base_cls=None): - """Convert an instance to its class source code representation.""" - cls = instance.__class__ - class_name = cls.__name__ - - # Start building class lines - class_lines = [] - if base_cls: - class_lines.append(f"class {class_name}({base_cls.__name__}):") - else: - class_lines.append(f"class {class_name}:") - - # Add docstring if it exists and differs from base - if cls.__doc__ and (not base_cls or cls.__doc__ != base_cls.__doc__): - class_lines.append(f' """{cls.__doc__}"""') - - # Add class-level attributes - class_attrs = { - name: value - for name, value in cls.__dict__.items() - if not name.startswith("__") - and not name == "_abc_impl" - and not callable(value) - and not ( - base_cls - and hasattr(base_cls, name) - and getattr(base_cls, name) == value - ) - } - - for name, value in class_attrs.items(): - if isinstance(value, str): - # multiline value - if "\n" in value: - escaped_value = value.replace( - '"""', r"\"\"\"" - ) # Escape triple quotes - class_lines.append(f' {name} = """{escaped_value}"""') - else: - class_lines.append(f" {name} = {json.dumps(value)}") - else: - class_lines.append(f" {name} = {repr(value)}") - - if class_attrs: - class_lines.append("") - - # Add methods - methods = { - name: func.__wrapped__ if hasattr(func, "__wrapped__") else func - for name, func in cls.__dict__.items() - if callable(func) - and ( - not base_cls - or not hasattr(base_cls, name) - or ( - isinstance(func, (staticmethod, classmethod)) - or ( - getattr(base_cls, name).__code__.co_code - != func.__code__.co_code - ) - ) - ) - } - - for name, method in methods.items(): - method_source = get_source(method) - # Clean up the indentation - method_lines = method_source.split("\n") - first_line = method_lines[0] - indent = len(first_line) - len(first_line.lstrip()) - method_lines = [line[indent:] for line in method_lines] - method_source = "\n".join( - [" " + line if line.strip() else line for line in method_lines] - ) - class_lines.append(method_source) - class_lines.append("") - - # Find required imports using ImportFinder - import_finder = ImportFinder() - import_finder.visit(ast.parse("\n".join(class_lines))) - required_imports = import_finder.packages - - # Build final code with imports - final_lines = [] - - # Add base class import if needed - if base_cls: - final_lines.append( - f"from {base_cls.__module__} import {base_cls.__name__}" - ) - - # Add discovered imports - for package in required_imports: - final_lines.append(f"import {package}") - - if final_lines: # Add empty line after imports - final_lines.append("") - - # Add the class code - final_lines.extend(class_lines) - - return "\n".join(final_lines) - - -def get_source(obj) -> str: - """Get the source code of a class or callable object (e.g.: function, method). - - First attempts to get the source code using `inspect.getsource`. - In a dynamic environment (e.g.: Jupyter, IPython), if this fails, - falls back to retrieving the source code from the current interactive shell session. - - Args: - obj: A class or callable object (e.g.: function, method) - - Returns: - str: The source code of the object, dedented and stripped - - Raises: - TypeError: If object is not a class or callable - OSError: If source code cannot be retrieved from any source - ValueError: If source cannot be found in IPython history - - Note: - TODO: handle Python standard REPL - """ - if not (isinstance(obj, type) or callable(obj)): - raise TypeError(f"Expected class or callable, got {type(obj)}") - - inspect_error = None - try: - # Handle dynamically created classes - source = getattr(obj, "__source__", None) or inspect.getsource(obj) - return dedent(source).strip() - except OSError as e: - # let's keep track of the exception to raise it if all further methods fail - inspect_error = e - try: - import IPython - - shell = IPython.get_ipython() - if not shell: - raise ImportError("No active IPython shell found") - all_cells = "\n".join(shell.user_ns.get("In", [])).strip() - if not all_cells: - raise ValueError("No code cells found in IPython session") - - tree = ast.parse(all_cells) - for node in ast.walk(tree): - if ( - isinstance(node, (ast.ClassDef, ast.FunctionDef)) - and node.name == obj.__name__ - ): - return dedent( - "\n".join( - all_cells.split("\n")[node.lineno - 1 : node.end_lineno] - ) - ).strip() - raise ValueError( - f"Could not find source code for {obj.__name__} in IPython history" - ) - except ImportError: - # IPython is not available, let's just raise the original inspect error - raise inspect_error - except ValueError as e: - # IPython is available but we couldn't find the source code, let's raise the error - raise e from inspect_error - - -def is_valid_name(name: str) -> bool: - """Check if a name is a valid Python identifier.""" - return ( - name.isidentifier() and not keyword.iskeyword(name) - if isinstance(name, str) - else False - ) diff --git a/quantmind/utils/agentic_ext.py b/quantmind/utils/agentic_ext.py deleted file mode 100644 index 8059ee0..0000000 --- a/quantmind/utils/agentic_ext.py +++ /dev/null @@ -1,554 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import ast -import base64 -import importlib.util -import inspect -import json -import keyword -import os -import re -import time -from functools import lru_cache -from io import BytesIO -from pathlib import Path -from textwrap import dedent -from typing import TYPE_CHECKING, Any - -import jinja2 - -if TYPE_CHECKING: - from .monitoring import AgentLogger - - -__all__ = ["AgentError"] - - -@lru_cache -def _is_package_available(package_name: str) -> bool: - return importlib.util.find_spec(package_name) is not None - - -BASE_BUILTIN_MODULES = [ - "collections", - "datetime", - "itertools", - "math", - "queue", - "random", - "re", - "stat", - "statistics", - "time", - "unicodedata", -] - - -def escape_code_brackets(text: str) -> str: - """Escapes square brackets in code segments while preserving Rich styling tags.""" - - def replace_bracketed_content(match): - content = match.group(1) - cleaned = re.sub( - r"bold|red|green|blue|yellow|magenta|cyan|white|black|italic|dim|\s|#[0-9a-fA-F]{6}", - "", - content, - ) - return f"\\[{content}\\]" if cleaned.strip() else f"[{content}]" - - return re.sub(r"\[([^\]]*)\]", replace_bracketed_content, text) - - -class AgentError(Exception): - """Base class for other agent-related exceptions.""" - - def __init__(self, message, logger: "AgentLogger"): - super().__init__(message) - self.message = message - logger.log_error(message) - - def dict(self) -> dict[str, str]: - return {"type": self.__class__.__name__, "message": str(self.message)} - - -class AgentParsingError(AgentError): - """Exception raised for errors in parsing in the agent.""" - - pass - - -class AgentExecutionError(AgentError): - """Exception raised for errors in execution in the agent.""" - - pass - - -class AgentMaxStepsError(AgentError): - """Exception raised for errors in execution in the agent.""" - - pass - - -class AgentToolCallError(AgentExecutionError): - """Exception raised for errors when incorrect arguments are passed to the tool.""" - - pass - - -class AgentToolExecutionError(AgentExecutionError): - """Exception raised for errors when executing a tool.""" - - pass - - -class AgentGenerationError(AgentError): - """Exception raised for errors in generation in the agent.""" - - pass - - -def make_json_serializable(obj: Any) -> Any: - """Recursive function to make objects JSON serializable.""" - if obj is None: - return None - elif isinstance(obj, (str, int, float, bool)): - # Try to parse string as JSON if it looks like a JSON object/array - if isinstance(obj, str): - try: - if (obj.startswith("{") and obj.endswith("}")) or ( - obj.startswith("[") and obj.endswith("]") - ): - parsed = json.loads(obj) - return make_json_serializable(parsed) - except json.JSONDecodeError: - pass - return obj - elif isinstance(obj, (list, tuple)): - return [make_json_serializable(item) for item in obj] - elif isinstance(obj, dict): - return {str(k): make_json_serializable(v) for k, v in obj.items()} - elif hasattr(obj, "__dict__"): - # For custom objects, convert their __dict__ to a serializable format - return { - "_type": obj.__class__.__name__, - **{k: make_json_serializable(v) for k, v in obj.__dict__.items()}, - } - else: - # For any other type, convert to string - return str(obj) - - -def parse_json_blob(json_blob: str) -> tuple[dict[str, str], str]: - """Extracts the JSON blob from the input and returns the JSON data and the rest of the input.""" - try: - first_accolade_index = json_blob.find("{") - last_accolade_index = [ - a.start() for a in list(re.finditer("}", json_blob)) - ][-1] - json_str = json_blob[first_accolade_index : last_accolade_index + 1] - json_data = json.loads(json_str, strict=False) - return json_data, json_blob[:first_accolade_index] - except IndexError: - raise ValueError("The model output does not contain any JSON blob.") - except json.JSONDecodeError as e: - place = e.pos - if json_blob[place - 1 : place + 2] == "},\n": - raise ValueError( - "JSON is invalid: you probably tried to provide multiple tool calls in one action. PROVIDE ONLY ONE TOOL CALL." - ) - raise ValueError( - f"The JSON blob you used is invalid due to the following error: {e}.\n" - f"JSON blob was: {json_blob}, decoding failed on that specific part of the blob:\n" - f"'{json_blob[place - 4 : place + 5]}'." - ) - - -def extract_code_from_text( - text: str, code_block_tags: tuple[str, str] -) -> str | None: - """Extract code from the LLM's output.""" - pattern = rf"{code_block_tags[0]}(.*?){code_block_tags[1]}" - matches = re.findall(pattern, text, re.DOTALL) - if matches: - return "\n\n".join(match.strip() for match in matches) - return None - - -def parse_code_blobs(text: str, code_block_tags: tuple[str, str]) -> str: - """Extract code blocs from the LLM's output. - - If a valid code block is passed, it returns it directly. - - Args: - text (`str`): LLM's output text to parse. - code_block_tags (`tuple[str, str]`): Tuple of code block tags. - - Returns: - `str`: Extracted code block. - - Raises: - ValueError: If no valid code block is found in the text. - """ - matches = extract_code_from_text(text, code_block_tags) - if not matches: # Fallback to markdown pattern - matches = extract_code_from_text(text, ("```(?:python|py)", "\n```")) - if matches: - return matches - # Maybe the LLM outputted a code blob directly - try: - ast.parse(text) - return text - except SyntaxError: - pass - - if "final" in text and "answer" in text: - raise ValueError( - dedent( - f""" - Your code snippet is invalid, because the regex pattern {code_block_tags[0]}(.*?){code_block_tags[1]} was not found in it. - Here is your code snippet: - {text} - It seems like you're trying to return the final answer, you can do it as follows: - {code_block_tags[0]} - final_answer("YOUR FINAL ANSWER HERE") - {code_block_tags[1]} - """ - ).strip() - ) - raise ValueError( - dedent( - f""" - Your code snippet is invalid, because the regex pattern {code_block_tags[0]}(.*?){code_block_tags[1]} was not found in it. - Here is your code snippet: - {text} - Make sure to include code with the correct pattern, for instance: - Thoughts: Your thoughts - {code_block_tags[0]} - # Your python code here - {code_block_tags[1]} - """ - ).strip() - ) - - -MAX_LENGTH_TRUNCATE_CONTENT = 20000 - - -def truncate_content( - content: str, max_length: int = MAX_LENGTH_TRUNCATE_CONTENT -) -> str: - if len(content) <= max_length: - return content - else: - return ( - content[: max_length // 2] - + f"\n..._This content has been truncated to stay below {max_length} characters_...\n" - + content[-max_length // 2 :] - ) - - -class ImportFinder(ast.NodeVisitor): - """Import finder class.""" - - def __init__(self): - self.packages = set() - - def visit_Import(self, node): - for alias in node.names: - # Get the base package name (before any dots) - base_package = alias.name.split(".")[0] - self.packages.add(base_package) - - def visit_ImportFrom(self, node): - if node.module: # for "from x import y" statements - # Get the base package name (before any dots) - base_package = node.module.split(".")[0] - self.packages.add(base_package) - - -def instance_to_source(instance, base_cls=None): - """Convert an instance to its class source code representation.""" - cls = instance.__class__ - class_name = cls.__name__ - - # Start building class lines - class_lines = [] - if base_cls: - class_lines.append(f"class {class_name}({base_cls.__name__}):") - else: - class_lines.append(f"class {class_name}:") - - # Add docstring if it exists and differs from base - if cls.__doc__ and (not base_cls or cls.__doc__ != base_cls.__doc__): - class_lines.append(f' """{cls.__doc__}"""') - - # Add class-level attributes - class_attrs = { - name: value - for name, value in cls.__dict__.items() - if not name.startswith("__") - and not name == "_abc_impl" - and not callable(value) - and not ( - base_cls - and hasattr(base_cls, name) - and getattr(base_cls, name) == value - ) - } - - for name, value in class_attrs.items(): - if isinstance(value, str): - # multiline value - if "\n" in value: - escaped_value = value.replace( - '"""', r"\"\"\"" - ) # Escape triple quotes - class_lines.append(f' {name} = """{escaped_value}"""') - else: - class_lines.append(f" {name} = {json.dumps(value)}") - else: - class_lines.append(f" {name} = {repr(value)}") - - if class_attrs: - class_lines.append("") - - # Add methods - methods = { - name: func.__wrapped__ if hasattr(func, "__wrapped__") else func - for name, func in cls.__dict__.items() - if callable(func) - and ( - not base_cls - or not hasattr(base_cls, name) - or ( - isinstance(func, (staticmethod, classmethod)) - or ( - getattr(base_cls, name).__code__.co_code - != func.__code__.co_code - ) - ) - ) - } - - for name, method in methods.items(): - method_source = get_source(method) - # Clean up the indentation - method_lines = method_source.split("\n") - first_line = method_lines[0] - indent = len(first_line) - len(first_line.lstrip()) - method_lines = [line[indent:] for line in method_lines] - method_source = "\n".join( - [" " + line if line.strip() else line for line in method_lines] - ) - class_lines.append(method_source) - class_lines.append("") - - # Find required imports using ImportFinder - import_finder = ImportFinder() - import_finder.visit(ast.parse("\n".join(class_lines))) - required_imports = import_finder.packages - - # Build final code with imports - final_lines = [] - - # Add base class import if needed - if base_cls: - final_lines.append( - f"from {base_cls.__module__} import {base_cls.__name__}" - ) - - # Add discovered imports - for package in required_imports: - final_lines.append(f"import {package}") - - if final_lines: # Add empty line after imports - final_lines.append("") - - # Add the class code - final_lines.extend(class_lines) - - return "\n".join(final_lines) - - -def get_source(obj) -> str: - """Get the source code of a class or callable object (e.g.: function, method). - - First attempts to get the source code using `inspect.getsource`. - In a dynamic environment (e.g.: Jupyter, IPython), if this fails, - falls back to retrieving the source code from the current interactive shell session. - - Args: - obj: A class or callable object (e.g.: function, method) - - Returns: - str: The source code of the object, dedented and stripped - - Raises: - TypeError: If object is not a class or callable - OSError: If source code cannot be retrieved from any source - ValueError: If source cannot be found in IPython history - - Note: - TODO: handle Python standard REPL - """ - if not (isinstance(obj, type) or callable(obj)): - raise TypeError(f"Expected class or callable, got {type(obj)}") - - inspect_error = None - try: - # Handle dynamically created classes - source = getattr(obj, "__source__", None) or inspect.getsource(obj) - return dedent(source).strip() - except OSError as e: - # let's keep track of the exception to raise it if all further methods fail - inspect_error = e - try: - import IPython - - shell = IPython.get_ipython() - if not shell: - raise ImportError("No active IPython shell found") - all_cells = "\n".join(shell.user_ns.get("In", [])).strip() - if not all_cells: - raise ValueError("No code cells found in IPython session") - - tree = ast.parse(all_cells) - for node in ast.walk(tree): - if ( - isinstance(node, (ast.ClassDef, ast.FunctionDef)) - and node.name == obj.__name__ - ): - return dedent( - "\n".join( - all_cells.split("\n")[node.lineno - 1 : node.end_lineno] - ) - ).strip() - raise ValueError( - f"Could not find source code for {obj.__name__} in IPython history" - ) - except ImportError: - # IPython is not available, let's just raise the original inspect error - raise inspect_error - except ValueError as e: - # IPython is available but we couldn't find the source code, let's raise the error - raise e from inspect_error - - -def encode_image_base64(image): - buffered = BytesIO() - image.save(buffered, format="PNG") - return base64.b64encode(buffered.getvalue()).decode("utf-8") - - -def make_image_url(base64_image): - return f"data:image/png;base64,{base64_image}" - - -def make_init_file(folder: str | Path): - os.makedirs(folder, exist_ok=True) - # Create __init__ - with open(os.path.join(folder, "__init__.py"), "w"): - pass - - -def is_valid_name(name: str) -> bool: - return ( - name.isidentifier() and not keyword.iskeyword(name) - if isinstance(name, str) - else False - ) - - -AGENT_GRADIO_APP_TEMPLATE = """import yaml -import os -from smolagents import GradioUI, {{ class_name }}, {{ agent_dict['model']['class'] }} - -# Get current directory path -CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) - -{% for tool in tools.values() -%} -from {{managed_agent_relative_path}}tools.{{ tool.name }} import {{ tool.__class__.__name__ }} as {{ tool.name | camelcase }} -{% endfor %} -{% for managed_agent in managed_agents.values() -%} -from {{managed_agent_relative_path}}managed_agents.{{ managed_agent.name }}.app import agent_{{ managed_agent.name }} -{% endfor %} - -model = {{ agent_dict['model']['class'] }}( -{% for key in agent_dict['model']['data'] if key != 'class' -%} - {{ key }}={{ agent_dict['model']['data'][key]|repr }}, -{% endfor %}) - -{% for tool in tools.values() -%} -{{ tool.name }} = {{ tool.name | camelcase }}() -{% endfor %} - -with open(os.path.join(CURRENT_DIR, "prompts.yaml"), 'r') as stream: - prompt_templates = yaml.safe_load(stream) - -{{ agent_name }} = {{ class_name }}( - model=model, - tools=[{% for tool_name in tools.keys() if tool_name != "final_answer" %}{{ tool_name }}{% if not loop.last %}, {% endif %}{% endfor %}], - managed_agents=[{% for subagent_name in managed_agents.keys() %}agent_{{ subagent_name }}{% if not loop.last %}, {% endif %}{% endfor %}], - {% for attribute_name, value in agent_dict.items() if attribute_name not in ["class", "model", "tools", "prompt_templates", "authorized_imports", "managed_agents", "requirements"] -%} - {{ attribute_name }}={{ value|repr }}, - {% endfor %}prompt_templates=prompt_templates -) -if __name__ == "__main__": - GradioUI({{ agent_name }}).launch() -""".strip() - - -def create_agent_gradio_app_template(): - env = jinja2.Environment( - loader=jinja2.BaseLoader(), undefined=jinja2.StrictUndefined - ) - env.filters["repr"] = repr - env.filters["camelcase"] = lambda value: "".join( - word.capitalize() for word in value.split("_") - ) - return env.from_string(AGENT_GRADIO_APP_TEMPLATE) - - -class RateLimiter: - """Simple rate limiter that enforces a minimum delay between consecutive requests. - - This class is useful for limiting the rate of operations such as API requests, - by ensuring that calls to `throttle()` are spaced out by at least a given interval - based on the desired requests per minute. - - If no rate is specified (i.e., `requests_per_minute` is None), rate limiting - is disabled and `throttle()` becomes a no-op. - - Args: - requests_per_minute (`float | None`): Maximum number of allowed requests per minute. - Use `None` to disable rate limiting. - """ - - def __init__(self, requests_per_minute: float | None = None): - self._enabled = requests_per_minute is not None - self._interval = 60.0 / requests_per_minute if self._enabled else 0.0 - self._last_call = 0.0 - - def throttle(self): - """Pause execution to respect the rate limit, if enabled.""" - if not self._enabled: - return - now = time.time() - elapsed = now - self._last_call - if elapsed < self._interval: - time.sleep(self._interval - elapsed) - self._last_call = time.time() diff --git a/quantmind/utils/monitoring.py b/quantmind/utils/monitoring.py deleted file mode 100644 index 3c9cec5..0000000 --- a/quantmind/utils/monitoring.py +++ /dev/null @@ -1,302 +0,0 @@ -#!/usr/bin/env python -# coding=utf-8 - -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import json -from dataclasses import dataclass, field -from enum import IntEnum - -from rich import box -from rich.console import Console, Group -from rich.panel import Panel -from rich.rule import Rule -from rich.syntax import Syntax -from rich.table import Table -from rich.text import Text -from rich.tree import Tree - -from .agentic_ext import escape_code_brackets - -__all__ = ["AgentLogger", "LogLevel", "Monitor", "TokenUsage", "Timing"] - - -@dataclass -class TokenUsage: - """Contains the token usage information for a given step or run.""" - - input_tokens: int - output_tokens: int - total_tokens: int = field(init=False) - - def __post_init__(self): - self.total_tokens = self.input_tokens + self.output_tokens - - def dict(self): - return { - "input_tokens": self.input_tokens, - "output_tokens": self.output_tokens, - "total_tokens": self.total_tokens, - } - - -@dataclass -class Timing: - """Contains the timing information for a given step or run.""" - - start_time: float - end_time: float | None = None - - @property - def duration(self): - return ( - None if self.end_time is None else self.end_time - self.start_time - ) - - def dict(self): - return { - "start_time": self.start_time, - "end_time": self.end_time, - "duration": self.duration, - } - - def __repr__(self) -> str: - return f"Timing(start_time={self.start_time}, end_time={self.end_time}, duration={self.duration})" - - -class Monitor: - """Monitor class.""" - - def __init__(self, tracked_model, logger): - self.step_durations = [] - self.tracked_model = tracked_model - self.logger = logger - self.total_input_token_count = 0 - self.total_output_token_count = 0 - - def get_total_token_counts(self) -> TokenUsage: - return TokenUsage( - input_tokens=self.total_input_token_count, - output_tokens=self.total_output_token_count, - ) - - def reset(self): - self.step_durations = [] - self.total_input_token_count = 0 - self.total_output_token_count = 0 - - def update_metrics(self, step_log): - """Update the metrics of the monitor. - - Args: - step_log ([`MemoryStep`]): Step log to update the monitor with. - """ - step_duration = step_log.timing.duration - self.step_durations.append(step_duration) - console_outputs = f"[Step {len(self.step_durations)}: Duration {step_duration:.2f} seconds" - - if step_log.token_usage is not None: - self.total_input_token_count += step_log.token_usage.input_tokens - self.total_output_token_count += step_log.token_usage.output_tokens - console_outputs += f"| Input tokens: {self.total_input_token_count:,} | Output tokens: {self.total_output_token_count:,}" - console_outputs += "]" - self.logger.log(Text(console_outputs, style="dim"), level=1) - - -class LogLevel(IntEnum): - """Log level enumerate class.""" - - OFF = -1 # No output - ERROR = 0 # Only errors - INFO = 1 # Normal output (default) - DEBUG = 2 # Detailed output - - -YELLOW_HEX = "#d4b702" - - -class AgentLogger: - """Agent Logger class.""" - - def __init__( - self, level: LogLevel = LogLevel.INFO, console: Console | None = None - ): - self.level = level - if console is None: - self.console = Console(highlight=False) - else: - self.console = console - - def log( # noqa: D417 - self, *args, level: int | str | LogLevel = LogLevel.INFO, **kwargs - ) -> None: - """Logs a message to the console. - - Args: - level (LogLevel, optional): Defaults to LogLevel.INFO. - """ - if isinstance(level, str): - level = LogLevel[level.upper()] - if level <= self.level: - self.console.print(*args, **kwargs) - - def log_error(self, error_message: str) -> None: - self.log( - escape_code_brackets(error_message), - style="bold red", - level=LogLevel.ERROR, - ) - - def log_markdown( - self, - content: str, - title: str | None = None, - level=LogLevel.INFO, - style=YELLOW_HEX, - ) -> None: - markdown_content = Syntax( - content, - lexer="markdown", - theme="github-dark", - word_wrap=True, - ) - if title: - self.log( - Group( - Rule( - "[bold italic]" + title, - align="left", - style=style, - ), - markdown_content, - ), - level=level, - ) - else: - self.log(markdown_content, level=level) - - def log_code( - self, title: str, content: str, level: int = LogLevel.INFO - ) -> None: - self.log( - Panel( - Syntax( - content, - lexer="python", - theme="monokai", - word_wrap=True, - ), - title="[bold]" + title, - title_align="left", - box=box.HORIZONTALS, - ), - level=level, - ) - - def log_rule(self, title: str, level: int = LogLevel.INFO) -> None: - self.log( - Rule( - "[bold]" + title, - characters="━", - style=YELLOW_HEX, - ), - level=LogLevel.INFO, - ) - - def log_task( - self, - content: str, - subtitle: str, - title: str | None = None, - level: LogLevel = LogLevel.INFO, - ) -> None: - self.log( - Panel( - f"\n[bold]{escape_code_brackets(content)}\n", - title="[bold]New run" + (f" - {title}" if title else ""), - subtitle=subtitle, - border_style=YELLOW_HEX, - subtitle_align="left", - ), - level=level, - ) - - def log_messages( - self, messages: list[dict], level: LogLevel = LogLevel.DEBUG - ) -> None: - messages_as_string = "\n".join( - [json.dumps(dict(message), indent=4) for message in messages] - ) - self.log( - Syntax( - messages_as_string, - lexer="markdown", - theme="github-dark", - word_wrap=True, - ), - level=level, - ) - - def visualize_agent_tree(self, agent): - def create_tools_section(tools_dict): - table = Table(show_header=True, header_style="bold") - table.add_column("Name", style="#1E90FF") - table.add_column("Description") - table.add_column("Arguments") - - for name, tool in tools_dict.items(): - args = [ - f"{arg_name} (`{info.get('type', 'Any')}`{', optional' if info.get('optional') else ''}): {info.get('description', '')}" - for arg_name, info in getattr(tool, "inputs", {}).items() - ] - table.add_row( - name, - getattr(tool, "description", str(tool)), - "\n".join(args), - ) - - return Group("🛠️ [italic #1E90FF]Tools:[/italic #1E90FF]", table) - - def get_agent_headline(agent, name: str | None = None): - name_headline = f"{name} | " if name else "" - return f"[bold {YELLOW_HEX}]{name_headline}{agent.__class__.__name__} | {agent.model.model_id}" - - def build_agent_tree(parent_tree, agent_obj): - """Recursively builds the agent tree.""" - parent_tree.add(create_tools_section(agent_obj.tools)) - - if agent_obj.managed_agents: - agents_branch = parent_tree.add( - "🤖 [italic #1E90FF]Managed agents:" - ) - for name, managed_agent in agent_obj.managed_agents.items(): - agent_tree = agents_branch.add( - get_agent_headline(managed_agent, name) - ) - if managed_agent.__class__.__name__ == "CodeAgent": - agent_tree.add( - f"✅ [italic #1E90FF]Authorized imports:[/italic #1E90FF] {managed_agent.additional_authorized_imports}" - ) - agent_tree.add( - f"📝 [italic #1E90FF]Description:[/italic #1E90FF] {managed_agent.description}" - ) - build_agent_tree(agent_tree, managed_agent) - - main_tree = Tree(get_agent_headline(agent)) - if agent.__class__.__name__ == "CodeAgent": - main_tree.add( - f"✅ [italic #1E90FF]Authorized imports:[/italic #1E90FF] {agent.additional_authorized_imports}" - ) - build_agent_tree(main_tree, agent) - self.console.print(main_tree) diff --git a/tests/brain/test_memory.py b/tests/brain/test_memory.py deleted file mode 100644 index d518331..0000000 --- a/tests/brain/test_memory.py +++ /dev/null @@ -1,93 +0,0 @@ -import unittest - -from quantmind.brain.memory import CallbackRegistry, Memory -from quantmind.models.memory import ActionStep, TaskStep, ToolCall -from quantmind.models.messages import ChatMessage, MessageRole -from quantmind.utils.monitoring import Timing, TokenUsage - - -class MemoryTestCase(unittest.TestCase): - """Test memory functionality.""" - - def test_memory_succinct_and_full_steps(self): - memory = Memory("system prompt") - task_step = TaskStep(task="Investigate signal") - action_step = ActionStep( - step_number=1, - timing=Timing(start_time=0.0, end_time=1.2), - model_input_messages=[ - ChatMessage( - role=MessageRole.USER, - content=[{"type": "text", "text": "What is the status?"}], - ) - ], - tool_calls=[ - ToolCall( - name="status_tool", - arguments={"region": "EMEA", "threshold": 0.5}, - id="call-1", - ) - ], - model_output="Status retrieved", - action_output={"result": "ok"}, - token_usage=TokenUsage(input_tokens=10, output_tokens=5), - ) - memory.steps.extend([task_step, action_step]) - - succinct_steps = memory.get_succinct_steps() - self.assertEqual(len(succinct_steps), 2) - self.assertNotIn("model_input_messages", succinct_steps[1]) - - full_steps = memory.get_full_steps() - self.assertEqual(len(full_steps), 2) - self.assertIn("model_input_messages", full_steps[1]) - self.assertEqual( - full_steps[1]["tool_calls"][0]["function"]["name"], - "status_tool", - ) - self.assertEqual(full_steps[1]["token_usage"]["total_tokens"], 15) - - def test_action_step_to_messages(self): - step = ActionStep( - step_number=2, - timing=Timing(start_time=5.0, end_time=6.0), - model_output="Calculation complete", - tool_calls=[ - ToolCall( - name="calc_tool", - arguments={"x": 1, "y": 2}, - id="calc-1", - ) - ], - observations="All good", - ) - - messages = step.to_messages() - self.assertEqual(messages[0].role, MessageRole.ASSISTANT) - self.assertEqual(messages[0].content[0]["text"], "Calculation complete") - self.assertEqual(messages[1].role, MessageRole.TOOL_CALL) - self.assertIn("Calling tools", messages[1].content[0]["text"]) - self.assertEqual(messages[-1].role, MessageRole.TOOL_RESPONSE) - self.assertIn("Observation", messages[-1].content[0]["text"]) - - def test_callback_registry_executes_registered_callbacks(self): - registry = CallbackRegistry() - captured = [] - - def on_action(step, agent=None): - captured.append((step.step_number, agent)) - - registry.register(ActionStep, on_action) - - dummy_step = ActionStep( - step_number=3, timing=Timing(start_time=0.0, end_time=0.1) - ) - registry.callback(dummy_step, agent="agent-instance") - - self.assertEqual(len(captured), 1) - self.assertEqual(captured[0][0], 3) - self.assertEqual(captured[0][1], "agent-instance") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/models/test_memory_models.py b/tests/models/test_memory_models.py deleted file mode 100644 index 35156b9..0000000 --- a/tests/models/test_memory_models.py +++ /dev/null @@ -1,108 +0,0 @@ -import unittest - -from quantmind.models.memory import ( - ActionStep, - PlanningStep, - TaskStep, - ToolCall, -) -from quantmind.models.messages import ChatMessage, MessageRole -from quantmind.utils.monitoring import Timing, TokenUsage - - -class MemoryModelsTestCase(unittest.TestCase): - """Test memory models functionality.""" - - def test_action_step_dict_serializes_tool_calls_and_tokens(self): - call = ToolCall( - name="status_tool", - arguments={"region": "APAC"}, - id="call-1", - ) - step = ActionStep( - step_number=1, - timing=Timing(start_time=0.0, end_time=0.4), - model_input_messages=[ - ChatMessage( - role=MessageRole.USER, - content=[{"type": "text", "text": "Check signals"}], - ) - ], - tool_calls=[call], - model_output="Signals retrieved", - action_output={"status": "ok"}, - token_usage=TokenUsage(input_tokens=7, output_tokens=3), - ) - - data = step.dict() - - self.assertEqual(data["tool_calls"], [call.dict()]) - self.assertEqual(data["token_usage"]["total_tokens"], 10) - self.assertEqual(data["model_output"], "Signals retrieved") - self.assertIsNone(data["observations"]) - - def test_action_step_to_messages_includes_output_and_observation(self): - step = ActionStep( - step_number=2, - timing=Timing(start_time=1.0, end_time=1.5), - model_output="Computation complete", - observations="Done", - tool_calls=[ - ToolCall( - name="compute", - arguments={"x": 1}, - id="compute-1", - ) - ], - ) - - messages = step.to_messages() - - self.assertEqual(messages[0].role, MessageRole.ASSISTANT) - self.assertEqual(messages[0].content[0]["text"], "Computation complete") - self.assertEqual(messages[1].role, MessageRole.TOOL_CALL) - self.assertIn("Calling tools", messages[1].content[0]["text"]) - self.assertEqual(messages[-1].role, MessageRole.TOOL_RESPONSE) - self.assertIn("Observation", messages[-1].content[0]["text"]) - - def test_planning_step_to_messages(self): - step = PlanningStep( - model_input_messages=[ - ChatMessage( - role=MessageRole.USER, - content=[{"type": "text", "text": "Plan it"}], - ) - ], - model_output_message=ChatMessage( - role=MessageRole.ASSISTANT, - content="Plan follows", - ), - plan="Step 1", - timing=Timing(start_time=2.0, end_time=2.5), - ) - - messages = step.to_messages() - - self.assertEqual(len(messages), 2) - self.assertEqual(messages[0].role, MessageRole.ASSISTANT) - self.assertEqual(messages[1].role, MessageRole.USER) - - def test_task_step_to_messages_handles_images(self): - class _Image: - def __init__(self, value: str): - self._value = value - - def tobytes(self): - return self._value - - fake_image = _Image("image-bytes") - step = TaskStep(task="Summarize", task_images=[fake_image]) - messages = step.to_messages() - - self.assertEqual(messages[0].role, MessageRole.USER) - self.assertEqual(messages[0].content[1]["type"], "image") - self.assertIs(messages[0].content[1]["image"], fake_image) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/models/test_messages_models.py b/tests/models/test_messages_models.py deleted file mode 100644 index 3e98bc2..0000000 --- a/tests/models/test_messages_models.py +++ /dev/null @@ -1,104 +0,0 @@ -import unittest - -from quantmind.models.messages import ( - ChatMessage, - ChatMessageStreamDelta, - ChatMessageToolCall, - ChatMessageToolCallFunction, - ChatMessageToolCallStreamDelta, - MessageRole, - agglomerate_stream_deltas, - get_clean_message_list, -) -from quantmind.utils.monitoring import TokenUsage - - -class MessagesModelsTestCase(unittest.TestCase): - """Test messages models functionality.""" - - def test_chat_message_from_dict_parses_tool_calls(self): - data = { - "role": MessageRole.ASSISTANT, - "content": "Computation done", - "tool_calls": [ - { - "function": {"name": "compute", "arguments": {"x": 1}}, - "id": "call-1", - "type": "function", - } - ], - } - msg = ChatMessage.from_dict(data) - - self.assertIsInstance(msg.tool_calls[0], ChatMessageToolCall) - self.assertEqual(msg.tool_calls[0].function.name, "compute") - self.assertEqual(msg.tool_calls[0].function.arguments, {"x": 1}) - - def test_agglomerate_stream_deltas_merges_content_and_tokens(self): - deltas = [ - ChatMessageStreamDelta( - content="First chunk ", - token_usage=TokenUsage(input_tokens=1, output_tokens=2), - ), - ChatMessageStreamDelta( - content="second chunk", - token_usage=TokenUsage(input_tokens=3, output_tokens=1), - ), - ChatMessageStreamDelta( - tool_calls=[ - ChatMessageToolCallStreamDelta( - index=0, - id="call-2", - type="function", - function=ChatMessageToolCallFunction( - name="analyse", - arguments='{"param": ', - ), - ) - ] - ), - ChatMessageStreamDelta( - tool_calls=[ - ChatMessageToolCallStreamDelta( - index=0, - function=ChatMessageToolCallFunction( - name="", - arguments='"value"}', - ), - ) - ] - ), - ] - - message = agglomerate_stream_deltas(deltas) - - self.assertEqual(message.content, "First chunk second chunk") - self.assertEqual(message.token_usage.total_tokens, 7) - self.assertEqual(len(message.tool_calls), 1) - self.assertEqual(message.tool_calls[0].id, "call-2") - self.assertEqual(message.tool_calls[0].function.name, "analyse") - self.assertEqual( - message.tool_calls[0].function.arguments, '{"param": "value"}' - ) - - def test_get_clean_message_list_merges_consecutive_roles(self): - messages = [ - ChatMessage( - role=MessageRole.USER, - content=[{"type": "text", "text": "Line one"}], - ), - ChatMessage( - role=MessageRole.USER, - content=[{"type": "text", "text": "Line two"}], - ), - ] - - result = get_clean_message_list(messages) - - self.assertEqual(len(result), 1) - self.assertEqual(result[0]["role"], MessageRole.USER) - self.assertEqual(result[0]["content"][0]["text"], "Line one\nLine two") - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/storage/test_local_storage.py b/tests/storage/test_local_storage.py deleted file mode 100644 index 98ba4c9..0000000 --- a/tests/storage/test_local_storage.py +++ /dev/null @@ -1,410 +0,0 @@ -"""Tests for enhanced storage functionality with efficient indexing.""" - -import json -import shutil -import tempfile -import unittest -from datetime import datetime, timezone -from pathlib import Path -from unittest.mock import Mock, patch - -from quantmind.config.storage import LocalStorageConfig -from quantmind.models.paper import Paper -from quantmind.storage.local_storage import LocalStorage - - -class TestEnhancedStorageWithIndexing(unittest.TestCase): - """Test enhanced storage functionality with efficient indexing.""" - - def setUp(self): - """Set up test environment.""" - self.temp_dir = Path(tempfile.mkdtemp()) - self.config = LocalStorageConfig( - storage_dir=self.temp_dir, download_timeout=1 - ) - self.storage = LocalStorage(self.config) - - def tearDown(self): - """Clean up test environment.""" - if self.temp_dir.exists(): - shutil.rmtree(self.temp_dir) - - def test_index_initialization(self): - """Test that indexes are properly initialized.""" - # Check that index files are created - self.assertTrue(self.storage._get_index_path("raw_files").exists()) - self.assertTrue(self.storage._get_index_path("knowledges").exists()) - self.assertTrue(self.storage._get_index_path("embeddings").exists()) - - # Check that indexes are empty initially - self.assertEqual(len(self.storage._raw_files_index), 0) - self.assertEqual(len(self.storage._knowledges_index), 0) - self.assertEqual(len(self.storage._embeddings_index), 0) - - def test_raw_file_indexing(self): - """Test raw file storage and indexing.""" - # Store a raw file - pdf_content = b"%PDF-1.4 test content" - file_path = self.storage.store_raw_file( - file_id="test_pdf", content=pdf_content, file_extension=".pdf" - ) - - # Check that index was updated - self.assertIn("test_pdf", self.storage._raw_files_index) - index_entry = self.storage._raw_files_index["test_pdf"] - self.assertEqual(index_entry["extension"], ".pdf") - - # Check that index file was saved - index_path = self.storage._get_index_path("raw_files") - self.assertTrue(index_path.exists()) - - with open(index_path, "r") as f: - saved_index = json.load(f) - self.assertIn("test_pdf", saved_index) - - def test_fast_raw_file_lookup(self): - """Test that raw file lookup uses index for fast retrieval.""" - # Store multiple files - for i in range(5): - content = f"test content {i}".encode() - self.storage.store_raw_file( - file_id=f"test_{i}", content=content, file_extension=".txt" - ) - - # Verify all files are in index - self.assertEqual(len(self.storage._raw_files_index), 5) - - # Test retrieval - should use index - retrieved_path = self.storage.get_raw_file("test_3") - self.assertIsNotNone(retrieved_path) - self.assertTrue(retrieved_path.exists()) - self.assertEqual(retrieved_path.suffix, ".txt") - - def test_knowledge_indexing(self): - """Test knowledge item storage and indexing.""" - paper = Paper( - title="Test Paper", - abstract="Test abstract", - authors=["Test Author"], - arxiv_id="test.001", - categories=["q-fin.CP"], - published_date=datetime.now(timezone.utc), - source="test", - ) - - # Store knowledge - paper_id = self.storage.store_knowledge(paper) - - # Check that index was updated - self.assertIn("test.001", self.storage._knowledges_index) - - # Check fast retrieval - retrieved_paper = self.storage.get_knowledge("test.001") - self.assertIsNotNone(retrieved_paper) - self.assertEqual(retrieved_paper.title, "Test Paper") - - def test_embedding_indexing(self): - """Test embedding storage and indexing.""" - embedding = [0.1, 0.2, 0.3, 0.4, 0.5] - - # Store embedding - self.storage.store_embedding("test_knowledge", embedding, "test_model") - - # Check that index was updated - self.assertIn("test_knowledge", self.storage._embeddings_index) - - # Check fast retrieval - retrieved_embedding = self.storage.get_embedding("test_knowledge") - self.assertIsNotNone(retrieved_embedding) - self.assertEqual(retrieved_embedding["embedding"], embedding) - self.assertEqual(retrieved_embedding["model"], "test_model") - - def test_index_persistence_across_restarts(self): - """Test that indexes persist across storage restarts.""" - # Store some data - pdf_content = b"test pdf" - self.storage.store_raw_file( - "test_pdf", content=pdf_content, file_extension=".pdf" - ) - - paper = Paper( - title="Test Paper", - abstract="Test abstract", - authors=["Test Author"], - arxiv_id="test.001", - source="test", - ) - self.storage.store_knowledge(paper) - - # Create new storage instance (simulating restart) - new_storage = LocalStorage(self.config) - - # Check that indexes were loaded - self.assertIn("test_pdf", new_storage._raw_files_index) - self.assertIn("test.001", new_storage._knowledges_index) - - # Check that retrieval still works - retrieved_pdf = new_storage.get_raw_file("test_pdf") - self.assertIsNotNone(retrieved_pdf) - - retrieved_paper = new_storage.get_knowledge("test.001") - self.assertIsNotNone(retrieved_paper) - - def test_index_rebuilding(self): - """Test index rebuilding functionality.""" - # Create files directly in filesystem (bypassing storage) - raw_file = self.config.raw_files_dir / "direct_file.pdf" - raw_file.write_bytes(b"direct pdf content") - - knowledge_file = self.config.knowledges_dir / "direct_knowledge.json" - knowledge_data = { - "id": "direct_knowledge", - "title": "Direct Knowledge", - "abstract": "Direct abstract", - "content_type": "generic", - "source": "direct", - } - knowledge_file.write_text(json.dumps(knowledge_data)) - - # Rebuild indexes - self.storage.rebuild_all_indexes() - - # Check that files were indexed - self.assertIn("direct_file", self.storage._raw_files_index) - self.assertIn("direct_knowledge", self.storage._knowledges_index) - - # Check retrieval works - retrieved_raw = self.storage.get_raw_file("direct_file") - self.assertIsNotNone(retrieved_raw) - - retrieved_knowledge = self.storage.get_knowledge("direct_knowledge") - self.assertIsNotNone(retrieved_knowledge) - - def test_index_cleanup_on_missing_files(self): - """Test that index is cleaned up when files are deleted externally.""" - # Store a file - self.storage.store_raw_file( - "test_file", content=b"test", file_extension=".txt" - ) - - # Verify it's in index - self.assertIn("test_file", self.storage._raw_files_index) - - # Delete file directly from filesystem - file_path = self.storage.get_raw_file("test_file") - file_path.unlink() - - # Try to retrieve - should clean up index - retrieved = self.storage.get_raw_file("test_file") - self.assertIsNone(retrieved) - - # Check that index was cleaned up - self.assertNotIn("test_file", self.storage._raw_files_index) - - def test_fallback_to_directory_scan(self): - """Test fallback to directory scan when file not in index.""" - # Create file directly in filesystem - raw_file = self.config.raw_files_dir / "fallback_test.pdf" - raw_file.write_bytes(b"fallback content") - - # File should not be in index initially - self.assertNotIn("fallback_test", self.storage._raw_files_index) - - # Try to retrieve - should find via directory scan and add to index - retrieved = self.storage.get_raw_file("fallback_test") - self.assertIsNotNone(retrieved) - - # Check that it was added to index - self.assertIn("fallback_test", self.storage._raw_files_index) - - def test_delete_operations_update_index(self): - """Test that delete operations properly update indexes.""" - # Store and then delete raw file - self.storage.store_raw_file( - "delete_test", content=b"test", file_extension=".txt" - ) - self.assertIn("delete_test", self.storage._raw_files_index) - - deleted = self.storage.delete_raw_file("delete_test") - self.assertTrue(deleted) - self.assertNotIn("delete_test", self.storage._raw_files_index) - - # Store and then delete knowledge - paper = Paper( - title="Delete Test", - abstract="Test", - arxiv_id="delete.001", - source="test", - ) - self.storage.store_knowledge(paper) - self.assertIn("delete.001", self.storage._knowledges_index) - - deleted = self.storage.delete_knowledge("delete.001") - self.assertTrue(deleted) - self.assertNotIn("delete.001", self.storage._knowledges_index) - - def test_get_all_knowledges_uses_index(self): - """Test that get_all_knowledges uses index for efficient iteration.""" - # Store multiple knowledge items - for i in range(3): - paper = Paper( - title=f"Paper {i}", - abstract=f"Abstract {i}", - arxiv_id=f"test.{i:03d}", - source="test", - ) - self.storage.store_knowledge(paper) - - # Get all knowledges - all_knowledges = list(self.storage.get_all_knowledges()) - - # Should have 3 items - self.assertEqual(len(all_knowledges), 3) - - # Check that we got the right items - titles = [k.title for k in all_knowledges] - self.assertIn("Paper 0", titles) - self.assertIn("Paper 1", titles) - self.assertIn("Paper 2", titles) - - def test_storage_info_includes_index_stats(self): - """Test that storage info includes index statistics.""" - # Store some test data - self.storage.store_raw_file( - "test_file", content=b"test", file_extension=".txt" - ) - - paper = Paper( - title="Test Paper", - abstract="Test", - arxiv_id="test.001", - source="test", - ) - self.storage.store_knowledge(paper) - - self.storage.store_embedding("test.001", [0.1, 0.2], "test_model") - - # Get storage info - info = self.storage.get_storage_info() - - # Check that index stats are included - self.assertIn("indexes", info) - indexes = info["indexes"] - - self.assertEqual(indexes["raw_files"]["entries"], 1) - self.assertEqual(indexes["knowledges"]["entries"], 1) - self.assertEqual(indexes["embeddings"]["entries"], 1) - - # Check that index file paths are included - self.assertIn("index_file", indexes["raw_files"]) - self.assertIn("index_file", indexes["knowledges"]) - self.assertIn("index_file", indexes["embeddings"]) - - def test_process_knowledge_paper(self): - """Test specialized Paper storage with indexing.""" - paper = Paper( - title="Test Paper", - abstract="Test abstract for paper", - authors=["Test Author"], - arxiv_id="test.001", - categories=["q-fin.CP"], - published_date=datetime.now(timezone.utc), - source="test", - ) - - # Store using specialized method - paper_id = self.storage.process_knowledge(paper) - - # Verify paper was stored and indexed - self.assertEqual(paper_id, "test.001") - self.assertIn("test.001", self.storage._knowledges_index) - - # Verify paper can be retrieved quickly - retrieved_paper = self.storage.get_knowledge(paper_id) - self.assertIsNotNone(retrieved_paper) - self.assertEqual(retrieved_paper.title, "Test Paper") - - @patch("requests.get") - def test_process_knowledge_paper_with_pdf_url(self, mock_requests): - """Test Paper storage with PDF URL and indexing.""" - # Mock requests to avoid real network calls - mock_response = Mock() - mock_response.content = b"%PDF-1.4 fake content" - mock_response.raise_for_status = Mock() - mock_requests.return_value = mock_response - - paper = Paper( - title="Paper with PDF URL", - abstract="Test paper with PDF URL", - authors=["Test Author"], - arxiv_id="test.002", - pdf_url="https://example.com/paper.pdf", - categories=["q-fin.CP"], - published_date=datetime.now(timezone.utc), - source="test", - ) - - # Store using specialized method - paper_id = self.storage.process_knowledge(paper) - - # Verify paper was stored and indexed - self.assertEqual(paper_id, "test.002") - self.assertIn("test.002", self.storage._knowledges_index) - - retrieved_paper = self.storage.get_knowledge(paper_id) - self.assertIsNotNone(retrieved_paper) - self.assertEqual( - retrieved_paper.pdf_url, "https://example.com/paper.pdf" - ) - - def test_store_raw_file_with_content(self): - """Test storing raw file from content bytes with indexing.""" - # Test PDF content - pdf_content = b"%PDF-1.4 test content" - - file_path = self.storage.store_raw_file( - file_id="test_pdf", content=pdf_content, file_extension=".pdf" - ) - - # Verify file was created and indexed - stored_path = Path(file_path) - self.assertTrue(stored_path.exists()) - self.assertEqual(stored_path.suffix, ".pdf") - self.assertIn("test_pdf", self.storage._raw_files_index) - - # Verify content - with open(stored_path, "rb") as f: - self.assertEqual(f.read(), pdf_content) - - def test_store_raw_file_validation(self): - """Test input validation for store_raw_file.""" - # Test missing both parameters - with self.assertRaises(ValueError): - self.storage.store_raw_file("test_id") - - # Test providing both parameters - with self.assertRaises(ValueError): - self.storage.store_raw_file( - "test_id", file_path=Path("dummy"), content=b"dummy" - ) - - def test_store_raw_file_backward_compatibility(self): - """Test that file copying still works with indexing.""" - # Create a temporary file to copy - temp_file = self.temp_dir / "source.txt" - temp_file.write_text("Source file content") - - # Store by copying - file_path = self.storage.store_raw_file( - file_id="copied_file", file_path=temp_file - ) - - # Verify file was copied and indexed - stored_path = Path(file_path) - self.assertTrue(stored_path.exists()) - self.assertEqual(stored_path.read_text(), "Source file content") - self.assertIn("copied_file", self.storage._raw_files_index) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/tagger/test_llm_tagger.py b/tests/tagger/test_llm_tagger.py deleted file mode 100644 index 61fe1d1..0000000 --- a/tests/tagger/test_llm_tagger.py +++ /dev/null @@ -1,391 +0,0 @@ -"""Unit tests for simplified LLM tagger.""" - -import json -import unittest -from unittest.mock import Mock, patch - -from quantmind.config import LLMTaggerConfig -from quantmind.models.paper import Paper -from quantmind.tagger.llm_tagger import LLMTagger - - -class TestLLMTagger(unittest.TestCase): - """Test cases for simplified LLM tagger.""" - - def setUp(self): - """Set up test fixtures.""" - self.sample_paper = Paper( - title="Deep Learning for Cryptocurrency Trading", - abstract="This paper presents LSTM networks for Bitcoin price prediction using sentiment analysis.", - authors=["John Doe", "Jane Smith"], - url="https://example.com/paper.pdf", - ) - - def test_tagger_initialization(self): - """Test tagger initialization with default parameters.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger() - - self.assertEqual(tagger.llm_type, "openai") - self.assertEqual(tagger.llm_name, "gpt-4o") - self.assertEqual(tagger.config.max_tags, 5) - self.assertEqual(tagger.config.llm_config.temperature, 0.0) - - def test_tagger_initialization_with_params(self): - """Test tagger initialization with custom parameters.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger( - config=LLMTaggerConfig.create( - model="gpt-3.5-turbo", - temperature=0.7, - max_tags=3, - custom_instructions="Custom instructions for tagging", - ) - ) - - self.assertEqual(tagger.llm_name, "gpt-3.5-turbo") - self.assertEqual(tagger.config.max_tags, 3) - self.assertEqual(tagger.config.llm_config.temperature, 0.7) - self.assertEqual( - tagger.config.llm_config.custom_instructions, - "Custom instructions for tagging", - ) - - def test_tagger_initialization_with_direct_config(self): - """Test tagger initialization with direct config creation.""" - from quantmind.config.llm import LLMConfig - - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - llm_config = LLMConfig( - model="claude-3-5-sonnet-20241022", - temperature=0.5, - max_tokens=3000, - api_key="test-key", - ) - - tagger_config = LLMTaggerConfig( - llm_config=llm_config, - max_tags=7, - custom_prompt="Analyze content: {content} and return {max_tags} tags", - ) - - tagger = LLMTagger(config=tagger_config) - - self.assertEqual(tagger.llm_name, "claude-3-5-sonnet-20241022") - self.assertEqual(tagger.config.max_tags, 7) - self.assertEqual(tagger.config.llm_config.temperature, 0.5) - self.assertEqual(tagger.config.llm_config.max_tokens, 3000) - self.assertEqual(tagger.config.llm_config.api_key, "test-key") - - def test_prepare_content(self): - """Test content preparation from paper.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger() - content = tagger._prepare_content(self.sample_paper) - - self.assertIn( - "Title: Deep Learning for Cryptocurrency Trading", content - ) - self.assertIn("Abstract: This paper presents LSTM", content) - - def test_build_default_prompt(self): - """Test default prompt construction.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger() - content = "Test content" - - prompt = tagger._build_prompt(content) - - self.assertIn("quantitative finance", prompt) - self.assertIn("Test content", prompt) - self.assertIn("5 relevant tags", prompt) - self.assertIn("JSON list", prompt) - - def test_build_prompt_with_custom_prompt(self): - """Test prompt construction with custom prompt.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger( - config=LLMTaggerConfig( - custom_prompt="Analyze the content and return 5 relevant tags: {content}" - ) - ) - content = "Test content" - - prompt = tagger._build_prompt(content) - - self.assertIn( - "Analyze the content and return 5 relevant tags", prompt - ) - self.assertIn("Test content", prompt) - - def test_build_custom_prompt_with_variables(self): - """Test custom prompt construction with variables.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - custom_prompt = "Analyze: {content} and return {max_tags} tags" - tagger = LLMTagger( - config=LLMTaggerConfig(custom_prompt=custom_prompt) - ) - content = "Test content" - - prompt = tagger._build_prompt(content) - - self.assertEqual(prompt, "Analyze: Test content and return 5 tags") - - def test_parse_tags_json(self): - """Test parsing tags from JSON response.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger() - response = ( - '["crypto", "machine learning", "lstm", "bitcoin", "trading"]' - ) - - tags = tagger._parse_tags(response) - - self.assertEqual(len(tags), 5) - self.assertIn("crypto", tags) - self.assertIn("machine learning", tags) - - def test_parse_tags_json_with_extra_text(self): - """Test parsing tags from JSON response with extra text.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger() - response = 'Here are the tags: ["crypto", "deep learning", "sentiment"] for this paper.' - - tags = tagger._parse_tags(response) - - self.assertEqual(len(tags), 3) - self.assertIn("crypto", tags) - self.assertIn("deep learning", tags) - - def test_parse_tags_fallback(self): - """Test fallback tag parsing from plain text.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger() - response = ( - '"crypto", "machine learning", "trading", "sentiment analysis"' - ) - - tags = tagger._parse_tags(response) - - self.assertTrue(len(tags) > 0) - self.assertIn("crypto", tags) - - def test_tag_paper_success(self): - """Test successful paper tagging.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - # Mock LLMBlock - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - # Mock LLM response - mock_llm_block.generate_text.return_value = ( - '["crypto", "lstm", "trading", "deep learning", "bitcoin"]' - ) - - tagger = LLMTagger() - result_paper = tagger.tag_paper(self.sample_paper) - - # Check that tags were added - self.assertTrue(len(result_paper.tags) > 0) - self.assertIn("crypto", result_paper.tags) - self.assertIn("llm_tagger", result_paper.meta_info["tagger"]) - - def test_tag_paper_no_llm_block(self): - """Test paper tagging when no LLM block is available.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_create.return_value = None - - tagger = LLMTagger() - tagger.llm_block = None - - result_paper = tagger.tag_paper(self.sample_paper) - - # Paper should be returned unchanged - self.assertEqual(result_paper.title, self.sample_paper.title) - self.assertEqual(len(result_paper.tags), 0) - - def test_extract_tags(self): - """Test tag extraction from arbitrary text.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - # Mock LLMBlock - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - # Mock LLM response - mock_llm_block.generate_text.return_value = ( - '["finance", "analysis", "data"]' - ) - - # Configure tagger to expect 3 tags - config = LLMTaggerConfig.create(max_tags=3) - tagger = LLMTagger(config=config) - - tags = tagger.extract_tags( - "Financial data analysis paper", "Finance Title" - ) - - self.assertEqual(len(tags), 3) - self.assertIn("finance", tags) - - def test_extract_tags_from_text_quoted(self): - """Test extracting tags from text with quoted items.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger() - text = 'The tags are "machine learning", "trading", "analysis"' - - tags = tagger._extract_tags_from_text(text) - - self.assertEqual(len(tags), 3) - self.assertIn("machine learning", tags) - self.assertIn("trading", tags) - - def test_extract_tags_from_text_comma_separated(self): - """Test extracting tags from comma-separated text.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger() - text = "machine learning, deep learning, trading algorithms, risk management" - - tags = tagger._extract_tags_from_text(text) - - self.assertTrue(len(tags) >= 3) - self.assertIn("machine learning", tags) - - def test_max_tags_limit(self): - """Test that tag count is limited to max_tags.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - tagger = LLMTagger(config=LLMTaggerConfig(max_tags=3)) - response = '["tag1", "tag2", "tag3", "tag4", "tag5"]' - - tags = tagger._parse_tags(response) - limited_tags = tags[: tagger.config.max_tags] - - self.assertEqual(len(limited_tags), 3) - - def test_llm_config_access(self): - """Test accessing LLM configuration through composition.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - config = LLMTaggerConfig.create( - model="gpt-4o-mini", - temperature=0.8, - max_tokens=2000, - api_key="test-api-key", - max_tags=8, - ) - - tagger = LLMTagger(config=config) - - # Test access to LLM config properties - self.assertEqual(tagger.config.llm_config.model, "gpt-4o-mini") - self.assertEqual(tagger.config.llm_config.temperature, 0.8) - self.assertEqual(tagger.config.llm_config.max_tokens, 2000) - self.assertEqual(tagger.config.llm_config.api_key, "test-api-key") - self.assertEqual(tagger.config.max_tags, 8) - - def test_provider_detection(self): - """Test LLM provider type detection.""" - with patch( - "quantmind.tagger.llm_tagger.create_llm_block" - ) as mock_create: - mock_llm_block = Mock() - mock_create.return_value = mock_llm_block - - # Test OpenAI - config = LLMTaggerConfig.create(model="gpt-4o") - tagger = LLMTagger(config=config) - self.assertEqual( - tagger.config.llm_config.get_provider_type(), "openai" - ) - - # Test Anthropic - config = LLMTaggerConfig.create(model="claude-3-5-sonnet-20241022") - tagger = LLMTagger(config=config) - self.assertEqual( - tagger.config.llm_config.get_provider_type(), "anthropic" - ) - - # Test Google - config = LLMTaggerConfig.create(model="gemini-1.5-pro") - tagger = LLMTagger(config=config) - self.assertEqual( - tagger.config.llm_config.get_provider_type(), "google" - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/tools/test_base_tool.py b/tests/tools/test_base_tool.py deleted file mode 100644 index 316497c..0000000 --- a/tests/tools/test_base_tool.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Sanity checks for the QuantMind tool decorator and helpers.""" - -import unittest - -from quantmind.tools._function_type_hints_utils import DocstringParsingException -from quantmind.tools.base import Tool, tool, validate_tool_arguments - - -@tool -def multiply(x: float, y: float) -> float: - """Multiply two numbers. - - Args: - x (float): Left operand. - y (float): Right operand. - - Returns: - float: Product of the two inputs. - """ - return x * y - - -class UppercaseTool(Tool): - """Simple concrete Tool subclass for serialization checks.""" - - name = "uppercase_tool" - description = "Uppercase string input." - inputs = { - "text": { - "type": "string", - "description": "Text to uppercase.", - } - } - output_type = "string" - - def forward(self, text: str) -> str: # noqa: D401 - self explanatory - return text.upper() - - -class StructuredTool(Tool): - """Tool returning structured output with an explicit schema.""" - - name = "structured_tool" - description = "Return the length of a string in a structured payload." - inputs = { - "text": { - "type": "string", - "description": "Input text to measure.", - } - } - output_type = "object" - output_schema = { - "type": "object", - "properties": { - "length": {"type": "integer"}, - }, - "required": ["length"], - } - - def forward(self, text: str) -> dict[str, int]: # noqa: D401 - return {"length": len(text)} - - -class ToolDecoratorTests(unittest.TestCase): - """Validate the lightweight tool decorator behaviour.""" - - def test_tool_requires_docstring(self) -> None: - """Functions without docstrings should be rejected.""" - - def missing_doc(a: int) -> int: - return a - - with self.assertRaises(DocstringParsingException): - tool(missing_doc) - - def test_tool_metadata_and_execution(self) -> None: - """Decorated tools expose schema metadata and execute correctly.""" - - @tool - def add(a: int, b: int) -> int: - """Add two integers. - - Args: - a (int): First operand. - b (int): Second operand. - - Returns: - int: Computed sum. - """ - return a + b - - self.assertEqual(add.description.strip(), "Add two integers.") - self.assertEqual(add.inputs["a"]["type"], "integer") - - validate_tool_arguments(add, {"a": 1, "b": 2}) - with self.assertRaises(ValueError): - validate_tool_arguments(add, {"a": 1}) - with self.assertRaises(TypeError): - validate_tool_arguments(add, {"a": "one", "b": 2}) - - self.assertEqual(add(a=3, b=4), 7) - self.assertEqual(add({"a": 5, "b": 6}), 11) - self.assertIsInstance(add, Tool) - - def test_optional_and_enum_inputs(self) -> None: - """Defaults impact required flag and enum choices validated.""" - - @tool - def choose_action(action: str, mode: str = "auto") -> str: - """Select an action. - - Args: - action (str): Action flag (choices: ["buy", "sell"]) - mode (str): Execution mode. - """ - return f"{mode}:{action}" - - self.assertEqual( - choose_action.inputs["action"]["enum"], ["buy", "sell"] - ) - - validate_tool_arguments(choose_action, {"action": "buy"}) - with self.assertRaises(ValueError): - validate_tool_arguments(choose_action, {"mode": "manual"}) - - def test_positional_invocation_rules(self) -> None: - """Only single-argument tools allow positional calls.""" - - @tool - def echo(text: str) -> str: - """Echo text. - - Args: - text (str): Text to return. - """ - return text - - self.assertEqual(echo("hello"), "hello") - - @tool - def concat(a: str, b: str) -> str: - """Concatenate strings. - - Args: - a (str): First part. - b (str): Second part. - """ - return a + b - - with self.assertRaises(TypeError): - concat("value") - - -class ToolRuntimeTests(unittest.TestCase): - """Cover behaviour of concrete Tool subclasses and serialization helpers.""" - - def test_multiply_tool_schema_and_validation(self) -> None: - """Decorated module-level tool exposes schema metadata and validation.""" - self.assertEqual(multiply.name, "multiply") - self.assertEqual(multiply.inputs["x"]["type"], "number") - - validate_tool_arguments(multiply, {"x": 1, "y": 2.5}) - with self.assertRaises(ValueError): - validate_tool_arguments(multiply, {"x": 1}) - with self.assertRaises(TypeError): - validate_tool_arguments(multiply, {"x": "bad", "y": 1}) - - self.assertEqual(multiply(x=2, y=3), 6) - - def test_structured_tool_prompts_and_call(self) -> None: - """StructuredTool generates descriptive prompts and handles dict inputs.""" - structured = StructuredTool() - code_prompt = structured.to_code_prompt() - self.assertIn("structured_tool", code_prompt) - self.assertIn( - "Important: This tool returns structured output!", code_prompt - ) - - calling_prompt = structured.to_tool_calling_prompt() - self.assertIn("structured_tool", calling_prompt) - self.assertIn("Returns an output of type: object", calling_prompt) - - result = structured({"text": "alpha"}) - self.assertEqual(result, {"length": 5}) - self.assertTrue(structured.is_initialized) - - def test_subclass_to_dict_roundtrip(self) -> None: - """Subclass tools serialize to code and can be rehydrated.""" - tool_instance = UppercaseTool() - tool_dict = tool_instance.to_dict() - - self.assertEqual(tool_dict["name"], "uppercase_tool") - self.assertIn("uppercase_tool", tool_dict["code"]) - self.assertIn("quantmind", tool_dict["requirements"]) - - reloaded = Tool.from_dict(tool_dict) - self.assertEqual(reloaded(text="abc"), "ABC") - - def test_decorated_tool_to_dict_contains_forward_source(self) -> None: - """SimpleTool export includes the wrapped forward definition.""" - exported = multiply.to_dict() - self.assertEqual(exported["name"], "multiply") - self.assertIn("class SimpleTool", exported["code"]) - self.assertIn("def forward(self, x: float, y: float)", exported["code"]) - - def test_invalid_tool_definition_detected_on_init(self) -> None: - """Invalid class attributes should raise during instantiation.""" - - class InvalidNameTool(Tool): - name = "invalid tool name" - description = "Bad name" - inputs = { - "value": { - "type": "string", - "description": "v", - } - } - output_type = "string" - - def forward( - self, value: str - ) -> str: # pragma: no cover - never called - return value - - with self.assertRaises(Exception): - InvalidNameTool() - - -if __name__ == "__main__": # pragma: no cover - unittest.main() diff --git a/tests/tools/test_function_type_hints_utils.py b/tests/tools/test_function_type_hints_utils.py deleted file mode 100644 index ec0edc4..0000000 --- a/tests/tools/test_function_type_hints_utils.py +++ /dev/null @@ -1,553 +0,0 @@ -# coding=utf-8 -# Copyright 2024 HuggingFace Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from typing import Any - -import pytest - -from quantmind.tools._function_type_hints_utils import ( - DocstringParsingException, - get_imports, - get_json_schema, -) - - -@pytest.fixture -def valid_func(): - """A well-formed function with docstring, type hints, and return block.""" - - def multiply(x: int, y: float) -> float: - """Multiplies two numbers. - - Args: - x: The first number. - y: The second number. - - Returns: - Product of x and y. - """ - return x * y - - return multiply - - -@pytest.fixture -def no_docstring_func(): - """Function with no docstring.""" - - def sample(x: int): - return x - - return sample - - -@pytest.fixture -def missing_arg_doc_func(): - """Function with docstring but missing an argument description.""" - - def add(x: int, y: int): # noqa: D417 - """Adds two numbers. - - Args: - x: The first number. - """ - return x + y - - return add - - -@pytest.fixture -def bad_return_func(): - """Function docstring with missing return description (allowed).""" - - def do_nothing(x: str | None = None): - """Does nothing. - - Args: - x: Some optional string. - """ - pass - - return do_nothing - - -@pytest.fixture -def complex_types_func(): # noqa: D103 - def process_data( - items: list[str], config: dict[str, float], point: tuple[int, int] - ) -> dict: - """Process some data. - - Args: - items: List of items to process. - config: Configuration parameters. - point: A position as (x,y). - - Returns: - Processed data result. - """ - return {"result": True} - - return process_data - - -@pytest.fixture -def optional_types_func(): # noqa: D103 - def process_with_optional( - required_arg: str, optional_arg: int | None = None - ) -> str: - """Process with optional argument. - - Args: - required_arg: A required string argument. - optional_arg: An optional integer argument. - - Returns: - Processing result. - """ - return "processed" - - return process_with_optional - - -@pytest.fixture -def enum_choices_func(): # noqa: D103 - def select_color(color: str) -> str: - """Select a color. - - Args: - color: The color to select (choices: ["red", "green", "blue"]) - - Returns: - Selected color. - """ - return color - - return select_color - - -@pytest.fixture -def union_types_func(): # noqa: D103 - def process_union(value: int | str) -> bool | str: - """Process a value that can be either int or string. - - Args: - value: An integer or string value. - - Returns: - Processing result. - """ - return True if isinstance(value, int) else "string result" - - return process_union - - -@pytest.fixture -def nested_types_func(): # noqa: D103 - def process_nested_data(data: list[dict[str, Any]]) -> list[str]: - """Process nested data structure. - - Args: - data: List of dictionaries to process. - - Returns: - List of processed results. - """ - return ["result"] - - return process_nested_data - - -@pytest.fixture -def typed_docstring_func(): # noqa: D103 - def calculate(x: int, y: float) -> float: - """Calculate something. - - Args: - x (int): An integer parameter with type in docstring. - y (float): A float parameter with type in docstring. - - Returns: - float: The calculated result. - """ - return x * y - - return calculate - - -@pytest.fixture -def mismatched_types_func(): # noqa: D103 - def convert(value: int) -> str: - """Convert a value. - - Args: - value (str): A string value (type mismatch with hint). - - Returns: - int: Converted value (type mismatch with hint). - """ - return str(value) - - return convert - - -@pytest.fixture -def complex_docstring_types_func(): # noqa: D103 - def process(data: dict[str, list[int]]) -> list[dict[str, Any]]: - """Process complex data. - - Args: - data (Dict[str, List[int]]): Nested structure with types. - - Returns: - List[Dict[str, Any]]: Processed results with types. - """ - return [{"result": sum(v) for k, v in data.items()}] - - return process - - -@pytest.fixture -def keywords_in_description_func(): # noqa: D103 - def process(value: str) -> str: - """Function with Args: or Returns: keywords in its description. - - Args: - value: A string value. - - Returns: - str: Processed value. - """ - return value.upper() - - return process - - -class TestGetJsonSchema: - """Test for get_json_schema function.""" - - def test_get_json_schema_example(self): - """Test for get_json_schema function.""" - - def fn(x: int, y: tuple[str, str, float] | None = None) -> None: - """Test function. - - Args: - x: The first input - y: The second input - """ - pass - - schema = get_json_schema(fn) - expected_schema = { - "name": "fn", - "description": "Test function.", - "parameters": { - "type": "object", - "properties": { - "x": {"type": "integer", "description": "The first input"}, - "y": { - "type": "array", - "description": "The second input", - "nullable": True, - "prefixItems": [ - {"type": "string"}, - {"type": "string"}, - {"type": "number"}, - ], - }, - }, - "required": ["x"], - }, - "return": {"type": "null"}, - } - assert ( - schema["function"]["parameters"]["properties"]["y"] - == expected_schema["parameters"]["properties"]["y"] - ) - assert schema["function"] == expected_schema - - @pytest.mark.parametrize( - "fixture_name,should_fail", - [ - ("valid_func", False), - # ('no_docstring_func', True), - # ('missing_arg_doc_func', True), - ("bad_return_func", False), - ], - ) - def test_get_json_schema(self, request, fixture_name, should_fail): - func = request.getfixturevalue(fixture_name) - schema = get_json_schema(func) - assert schema["type"] == "function" - assert "function" in schema - assert "parameters" in schema["function"] - - @pytest.mark.parametrize( - "fixture_name,should_fail", - [ - # ('valid_func', False), - ("no_docstring_func", True), - ("missing_arg_doc_func", True), - # ('bad_return_func', False), - ], - ) - def test_get_json_schema_raises(self, request, fixture_name, should_fail): - func = request.getfixturevalue(fixture_name) - with pytest.raises(DocstringParsingException): - get_json_schema(func) - - @pytest.mark.parametrize( - "fixture_name,expected_properties", - [ - ("valid_func", {"x": "integer", "y": "number"}), - ("bad_return_func", {"x": "string"}), - ], - ) - def test_property_types(self, request, fixture_name, expected_properties): - """Test that property types are correctly mapped.""" - func = request.getfixturevalue(fixture_name) - schema = get_json_schema(func) - - properties = schema["function"]["parameters"]["properties"] - for prop_name, expected_type in expected_properties.items(): - assert properties[prop_name]["type"] == expected_type - - def test_schema_basic_structure(self, valid_func): - """Test that basic schema structure is correct.""" - schema = get_json_schema(valid_func) - # Check schema type - assert schema["type"] == "function" - assert "function" in schema - # Check function schema - function_schema = schema["function"] - assert function_schema["name"] == "multiply" - assert "description" in function_schema - assert function_schema["description"] == "Multiplies two numbers." - # Check parameters schema - assert "parameters" in function_schema - params = function_schema["parameters"] - assert params["type"] == "object" - assert "properties" in params - assert "required" in params - assert set(params["required"]) == {"x", "y"} - properties = params["properties"] - assert properties["x"]["type"] == "integer" - assert properties["y"]["type"] == "number" - # Check return schema - assert "return" in function_schema - return_schema = function_schema["return"] - assert return_schema["type"] == "number" - assert return_schema["description"] == "Product of x and y." - - def test_complex_types(self, complex_types_func): - """Test schema generation for complex types.""" - schema = get_json_schema(complex_types_func) - properties = schema["function"]["parameters"]["properties"] - # Check list type - assert properties["items"]["type"] == "array" - # Check dict type - assert properties["config"]["type"] == "object" - # Check tuple type - assert properties["point"]["type"] == "array" - assert len(properties["point"]["prefixItems"]) == 2 - assert properties["point"]["prefixItems"][0]["type"] == "integer" - assert properties["point"]["prefixItems"][1]["type"] == "integer" - - def test_optional_types(self, optional_types_func): - """Test schema generation for optional arguments.""" - schema = get_json_schema(optional_types_func) - params = schema["function"]["parameters"] - # Required argument should be in required list - assert "required_arg" in params["required"] - # Optional argument should not be in required list - assert "optional_arg" not in params["required"] - # Optional argument should be nullable - assert params["properties"]["optional_arg"]["nullable"] is True - assert params["properties"]["optional_arg"]["type"] == "integer" - - def test_enum_choices(self, enum_choices_func): - """Test schema generation for enum choices in docstring.""" - schema = get_json_schema(enum_choices_func) - color_prop = schema["function"]["parameters"]["properties"]["color"] - assert "enum" in color_prop - assert color_prop["enum"] == ["red", "green", "blue"] - - def test_union_types(self, union_types_func): - """Test schema generation for union types.""" - schema = get_json_schema(union_types_func) - value_prop = schema["function"]["parameters"]["properties"]["value"] - return_prop = schema["function"]["return"] - # Check union in parameter - assert len(value_prop["type"]) == 2 - # Check union in return type: should be converted to "any" - assert return_prop["type"] == "any" - - def test_nested_types(self, nested_types_func): - """Test schema generation for nested complex types.""" - schema = get_json_schema(nested_types_func) - data_prop = schema["function"]["parameters"]["properties"]["data"] - assert data_prop["type"] == "array" - - def test_typed_docstring_parsing(self, typed_docstring_func): - """Test parsing of docstrings with type annotations.""" - schema = get_json_schema(typed_docstring_func) - # Type hints should take precedence over docstring types - assert ( - schema["function"]["parameters"]["properties"]["x"]["type"] - == "integer" - ) - assert ( - schema["function"]["parameters"]["properties"]["y"]["type"] - == "number" - ) - # Description should be extracted correctly - assert ( - schema["function"]["parameters"]["properties"]["x"]["description"] - == "An integer parameter with type in docstring." - ) - assert ( - schema["function"]["parameters"]["properties"]["y"]["description"] - == "A float parameter with type in docstring." - ) - # Return type and description should be correct - assert schema["function"]["return"]["type"] == "number" - assert ( - schema["function"]["return"]["description"] - == "The calculated result." - ) - - def test_mismatched_docstring_types(self, mismatched_types_func): - """Test that type hints take precedence over docstring types when they conflict.""" - schema = get_json_schema(mismatched_types_func) - # Type hints should take precedence over docstring types - assert ( - schema["function"]["parameters"]["properties"]["value"]["type"] - == "integer" - ) - # Return type from type hint should be used, not docstring - assert schema["function"]["return"]["type"] == "string" - - def test_complex_docstring_types(self, complex_docstring_types_func): - """Test parsing of complex type annotations in docstrings.""" - schema = get_json_schema(complex_docstring_types_func) - # Check that complex nested type is parsed correctly from type hints - data_prop = schema["function"]["parameters"]["properties"]["data"] - assert data_prop["type"] == "object" - # Check return type - return_prop = schema["function"]["return"] - assert return_prop["type"] == "array" - # Description should include the type information from docstring - assert data_prop["description"] == "Nested structure with types." - assert return_prop["description"] == "Processed results with types." - - @pytest.mark.parametrize( - "fixture_name,expected_description", - [ - ( - "typed_docstring_func", - "An integer parameter with type in docstring.", - ), - ("complex_docstring_types_func", "Nested structure with types."), - ], - ) - def test_type_in_description_handling( - self, request, fixture_name, expected_description - ): - """Test that type information in docstrings is preserved in description.""" - func = request.getfixturevalue(fixture_name) - schema = get_json_schema(func) - # First parameter description should contain the expected text - first_param_name = list( - schema["function"]["parameters"]["properties"].keys() - )[0] - assert ( - schema["function"]["parameters"]["properties"][first_param_name][ - "description" - ] - == expected_description - ) - - def test_with_special_words_in_description_func( - self, keywords_in_description_func - ): - schema = get_json_schema(keywords_in_description_func) - assert ( - schema["function"]["description"] - == "Function with Args: or Returns: keywords in its description." - ) - - -class TestGetImport: - """Test for get_imports function.""" - - @pytest.mark.parametrize( - "code, expected", - [ - ( - """ - import numpy - import pandas - """, - ["numpy", "pandas"], - ), - # From imports - ( - """ - from torch import nn - from transformers import AutoModel - """, - ["torch", "transformers"], - ), - # Mixed case with nested imports - ( - """ - import numpy as np - from torch.nn import Linear - import os.path - """, - ["numpy", "torch", "os"], - ), - # Try/except block (should be filtered) - ( - """ - try: - import torch - except ImportError: - pass - import numpy - """, - ["numpy"], - ), - # Flash attention block (should be filtered) - ( - """ - if is_flash_attn_2_available(): - from flash_attn import flash_attn_func - import transformers - """, - ["transformers"], - ), - # Relative imports (should be excluded) - ( - """ - from .utils import helper - from ..models import transformer - """, - [], - ), - ], - ) - def test_get_imports(self, code: str, expected: list[str]): - assert sorted(get_imports(code)) == sorted(expected) diff --git a/tests/tools/test_tool_validation_module.py b/tests/tools/test_tool_validation_module.py deleted file mode 100644 index 7d4ebcf..0000000 --- a/tests/tools/test_tool_validation_module.py +++ /dev/null @@ -1,215 +0,0 @@ -import ast -import unittest -from textwrap import dedent - -from quantmind.tools import Tool -from quantmind.tools._tool_validation import ( - MethodChecker, - validate_tool_attributes, -) - -UNDEFINED_VARIABLE = "undefined" - - -class ValidTool(Tool): - """Valid tool.""" - - name = "valid_tool" - description = "A valid tool" - inputs = { - "input": { - "type": "string", - "description": "input payload", - "required": True, - } - } - output_type = "string" - simple_attr = "string" - dict_attr = {"key": "value"} - - def __init__(self, optional_param: str = "default") -> None: - super().__init__() - self.param = optional_param - - def forward(self, input: str) -> str: - return input.upper() - - -class InvalidToolName(Tool): - """Invalid tool name.""" - - name = "invalid tool name" - description = "Tool with invalid name" - inputs = { - "input": { - "type": "string", - "description": "input payload", - "required": True, - } - } - output_type = "string" - - def forward(self, input: str) -> str: - return input - - -class InvalidToolComplexAttrs(Tool): - """Invalid tool complex attributes.""" - - name = "invalid_tool" - description = "Tool with complex class attributes" - inputs = { - "input": { - "type": "string", - "description": "input payload", - "required": True, - } - } - output_type = "string" - complex_attr = [x for x in range(3)] - - def forward(self, input: str) -> str: - return input - - -class InvalidToolRequiredParams(Tool): - """Invalid tool required parameters.""" - - name = "invalid_tool" - description = "Tool with required params" - inputs = { - "input": { - "type": "string", - "description": "input payload", - "required": True, - } - } - output_type = "string" - - def __init__(self, required_param: str, kwarg1: int = 1) -> None: - super().__init__() - self.param = required_param - - def forward(self, input: str) -> str: - return input - - -class InvalidToolNonLiteralDefaultParam(Tool): - """Invalid tool non-literal default parameter.""" - - name = "invalid_tool" - description = "Tool with non-literal default parameter value" - inputs = { - "input": { - "type": "string", - "description": "input payload", - "required": True, - } - } - output_type = "string" - - def __init__(self, default_param: str = UNDEFINED_VARIABLE) -> None: - super().__init__() - self.default_param = default_param - - def forward(self, input: str) -> str: - return input - - -class InvalidToolUndefinedNames(Tool): - """Invalid tool undefined names.""" - - name = "invalid_tool" - description = "Tool with undefined names" - inputs = { - "input": { - "type": "string", - "description": "input payload", - "required": True, - } - } - output_type = "string" - - def forward(self, input: str) -> str: - return UNDEFINED_VARIABLE - - -class MultipleAssignmentsTool(Tool): - """Multiple assignments tool.""" - - name = "multiple_assignments_tool" - description = "Tool with multiple assignments" - inputs = { - "input": { - "type": "string", - "description": "input payload", - "required": True, - } - } - output_type = "string" - - def forward(self, input: str) -> str: - first, second = "1", "2" - return input + first + second - - -class TestToolValidation(unittest.TestCase): - """Test tool validation.""" - - def test_validate_tool_attributes_valid(self) -> None: - self.assertIsNone(validate_tool_attributes(ValidTool)) - - def test_invalid_tool_name(self) -> None: - with self.assertRaisesRegex( - ValueError, - "Class attribute 'name' must be a valid Python identifier", - ): - validate_tool_attributes(InvalidToolName) - - def test_complex_class_attribute(self) -> None: - with self.assertRaisesRegex( - ValueError, "Complex attributes should be defined in __init__" - ): - validate_tool_attributes(InvalidToolComplexAttrs) - - def test_required_init_parameter(self) -> None: - with self.assertRaisesRegex( - ValueError, "Parameters in __init__ must have default values" - ): - validate_tool_attributes(InvalidToolRequiredParams) - - def test_non_literal_default(self) -> None: - with self.assertRaisesRegex( - ValueError, - "Parameters in __init__ must have literal default values", - ): - validate_tool_attributes(InvalidToolNonLiteralDefaultParam) - - def test_undefined_names(self) -> None: - with self.assertRaisesRegex( - ValueError, "Name 'UNDEFINED_VARIABLE' is undefined" - ): - validate_tool_attributes(InvalidToolUndefinedNames) - - def test_multiple_assignments_allowed(self) -> None: - self.assertIsNone(validate_tool_attributes(MultipleAssignmentsTool)) - - -class TestMethodChecker(unittest.TestCase): - """Test method checker.""" - - def test_multiple_assignments(self) -> None: - source_code = dedent( - """ - def forward(self) -> str: - a, b = "1", "2" - return a + b - """ - ) - method_checker = MethodChecker(set()) - method_checker.visit(ast.parse(source_code)) - self.assertEqual(method_checker.errors, []) - - -if __name__ == "__main__": # pragma: no cover - unittest.main() diff --git a/tests/tools/test_utils_module.py b/tests/tools/test_utils_module.py deleted file mode 100644 index 2bdf031..0000000 --- a/tests/tools/test_utils_module.py +++ /dev/null @@ -1,74 +0,0 @@ -"""Unit tests covering QuantMind tool utility helpers.""" - -import ast -import unittest - -from quantmind.tools import Tool -from quantmind.tools.utils import ( - ImportFinder, - get_source, - instance_to_source, - is_valid_name, -) - - -class DummyTool(Tool): - """Minimal concrete tool used to verify source serialization.""" - - name = "dummy_tool" - description = "Example tool for instance_to_source" - inputs = { - "input": { - "type": "string", - "description": "Payload text", - "required": True, - } - } - output_type = "string" - long_text = "Line one\nLine two" - - def forward(self, input: str) -> str: - return input.upper() - - -class UtilsTests(unittest.TestCase): - """Validate helper behaviours mirroring smolagents utility tests.""" - - def test_import_finder_collects_base_modules(self) -> None: - """Ensure ImportFinder tracks unique top-level package names.""" - code = "import numpy as np\nfrom pandas.core.frame import DataFrame\n" - finder = ImportFinder() - finder.visit(ast.parse(code)) - self.assertEqual(finder.packages, {"numpy", "pandas"}) - - def test_instance_to_source_includes_base_import(self) -> None: - """instance_to_source emits base class import and method body.""" - tool_source = instance_to_source(DummyTool(), base_cls=Tool) - self.assertIn("from quantmind.tools.base import Tool", tool_source) - self.assertIn("class DummyTool(Tool):", tool_source) - self.assertIn("def forward(self, input: str) -> str:", tool_source) - self.assertIn("return input.upper()", tool_source) - - def test_get_source_standard_function(self) -> None: - """Function source is retrieved and dedented by get_source.""" - - def helper(value: int) -> int: - return value + 1 - - expected = "def helper(value: int) -> int:\n return value + 1" - self.assertEqual(get_source(helper), expected) - - def test_get_source_rejects_non_callable(self) -> None: - """get_source raises TypeError for unsupported inputs.""" - with self.assertRaises(TypeError): - get_source(42) # type: ignore[arg-type] - - def test_is_valid_name(self) -> None: - """Names must be valid identifiers and not keywords.""" - self.assertTrue(is_valid_name("valid_name")) - self.assertFalse(is_valid_name("invalid name")) - self.assertFalse(is_valid_name("for")) - - -if __name__ == "__main__": # pragma: no cover - unittest.main() From 2ba79a4b833e83eec030bde7e028c26c256a2d1e Mon Sep 17 00:00:00 2001 From: pkuwkl Date: Sat, 25 Apr 2026 22:26:37 +0800 Subject: [PATCH 2/3] docs(claude): refine CLAUDE.md with post-pivot guidance Rewrites CLAUDE.md so a future Claude session opening this repo can quickly understand the architectural direction (lib on top of OpenAI Agents SDK, NOT a framework), the transitional module map, the conventions to follow, and the explicit anti-patterns to avoid. New sections: - Target Architecture (the 5 future modules + magic.py) - Current Repository State (transitional module table) - Architecture Principles (8 principles distilled from design doc) - Conventions When Editing (schemas / configs / tools / memory / tests / imports) - Things NOT to Do (8 explicit anti-patterns including no CLI, no agent framework rebuild, no batch+memory mixing in MVP) - Reference Material (SDK docs URLs + archive branch) - Roadmap table (PR2-PR6+) Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 219 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 113 insertions(+), 106 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c90ed3a..2ba0340 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,127 +1,134 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when working in this repository. ## Project Overview QuantMind is an intelligent knowledge extraction and retrieval framework for quantitative -finance. It is being repositioned as a domain library that runs on top of OpenAI Agents -SDK rather than as a self-contained agent framework. The next-step architecture -introduces these top-level modules: +finance. As of 2026-04, it is being **repositioned as a domain library that runs on top +of OpenAI Agents SDK**, rather than as a self-contained agent framework. -- `flows/` — e2e processing pipelines (Agent runtime delegated to OpenAI Agents SDK) -- `knowledge/` — Pydantic-based knowledge schema standard -- `preprocess/` — fetching and formatting helpers (PDF/HTML → markdown, etc.) -- `mind/` — QuantMind's distinctive cognitive layer (working memory MVP first) -- `configs/` — centralized flow/input config types -- `magic.py` — natural-language → (input, cfg) resolver +The pre-pivot agent runtime (`brain/`, `tools/`, `storage/`, `tagger/`, custom Tool ABC, +custom MultiStepAgent / Memory) was removed in PR #70. A full snapshot of the removed +code is preserved on the `archive/agent-runtime-final` branch on origin — reference it +if you need historical context, never resurrect it into master. -Until those modules land, the repository is in a transitional state. PR1 removes the -self-built agent runtime so subsequent PRs can build the new architecture from a clean -slate. +## Target Architecture (post-migration) + +``` +quantmind/ +├── flows/ # e2e pipeline functions (paper_flow, news_flow, ...) +├── knowledge/ # Pydantic schemas (KnowledgeItem subclasses: Paper, News, ...) +├── preprocess/ # fetch (arxiv/http/doi/local) + format (pdf/html/markdown) +├── mind/ # cognitive layer; mind/memory/ is the MVP (filesystem-backed) +├── configs/ # centralized cfg + input types (BaseFlowCfg + per-flow types) +├── magic.py # resolve_magic_input: natural language -> (input, cfg) +└── utils/ # logger only +``` + +Key principle: QuantMind does NOT rebuild Agent runtime, lifecycle hooks, tracing, +multi-agent handoff, or tool framework. Those come from `openai-agents`. + +## Current Repository State (transitional, after PR #70) + +Surviving modules — these still work but will be replaced or migrated in PR2-PR4: + +| Module | Status | Replacement | +|--------|--------|-------------| +| `quantmind/flow/` | active | `flows/` in PR4 | +| `quantmind/parsers/` | active | `preprocess/format/` in PR3 | +| `quantmind/sources/` | active | `preprocess/fetch/` in PR3 | +| `quantmind/config/` | active | `configs/` in PR2 | +| `quantmind/llm/` | active | deleted in PR4 (use SDK + `openai` directly) | +| `quantmind/models/{content,paper,analysis}.py` | active | move to `knowledge/` in PR2 | +| `quantmind/utils/logger.py` | active | permanent | ## Development Commands -### Environment Setup +### Environment + ```bash -# Create and activate virtual environment uv venv -source .venv/bin/activate # macOS/Linux -.venv\Scripts\activate # Windows - -# Install dependencies +source .venv/bin/activate uv pip install -e . ``` -### Code Quality +### Lint + Tests + ```bash -# Lint and format code -./scripts/lint.sh -# Or manually: ruff format . ruff check . - -# Run tests -./scripts/unittest.sh -# Or manually: -pytest tests # all tests -pytest tests/quantmind/ # new quantmind tests -pytest tests/quantmind/models/ # specific module +pytest tests/ ``` -## Current Modules (transitional, PR1) - -After PR1's removal, the surviving modules are: - -- **flow/**: Existing flow scaffolding (will be replaced by `flows/` in a later PR) -- **parsers/**: PDF / Llama parser helpers (will move to `preprocess/format/`) -- **sources/**: ArXiv source (fetch logic will move to `preprocess/fetch/`) -- **config/**: Configuration management (will be replaced by `configs/`) -- **llm/**: LLM block + embedding helpers (will be removed once `flow/` migrates) -- **models/**: `Paper`, `BaseContent`, `KnowledgeItem`, `analysis` (will move to `knowledge/`) -- **utils/**: `logger.py` (kept long-term) plus tmp helpers - -These modules continue to compile and ship as-is in PR1; their replacements arrive in -PR2 (`knowledge/` + `configs/`), PR3 (`preprocess/`), and PR4 (`flows/` + drop `flow/` `llm/`). - -## Key Dependencies - -### Core Dependencies -- Pydantic for data validation -- PyMuPDF / Marker for PDF processing -- ArXiv API client -- LiteLLM for multi-provider LLM access -- YAML / Requests / httpx for configuration and IO - -### Optional Dependencies -- OpenAI API -- llama-cloud-services (Llama parser) - -## Development Guidelines - -### Code Style -- Use Pydantic models for data validation -- Follow dependency injection patterns -- Use abstract base classes for extensibility -- Implement comprehensive error handling -- Write descriptive docstrings (Google style) - -### Testing -- Unit tests in `tests/quantmind/` -- Mock external dependencies -- Test both success and failure cases -- Use pytest fixtures for common setups - -### Configuration -- Use structured configuration via `quantmind.config.settings` -- Support environment variable overrides -- Validate configuration at startup -- Provide sensible defaults - -### Architecture Principles -- **Separation of Concerns**: Each component has a single responsibility -- **Dependency Injection**: Components are configurable and testable -- **Pipeline Orchestration**: Workflow management with task dependencies -- **Quality Control**: Built-in deduplication and validation -- **Extensibility**: Easy to add new sources, parsers, taggers, storage - -## User Development Guidance - -- Config should add in `quantmind/config` (until `configs/` lands in a later PR) -- Data models should add in `quantmind/models` (until `knowledge/` lands) -- Do not use `Dict[str, Any]` in initialize functions — not type safe. -- Do not overdesign — implement the basic and straightforward code, refactor later. -- Tests in `tests//`, inherit `unittest.TestCase`. - -## PR1 Cleanup (2026-04-25) - -PR1 removes the self-built agent runtime to make room for the OpenAI Agents SDK -migration. Removed in PR1: - -- `quantmind/brain/`, `quantmind/tools/`, `quantmind/storage/`, `quantmind/tagger/` -- `quantmind/models/{agent,memory,messages}.py` -- `quantmind/utils/{agentic_ext,monitoring}.py` -- vendored `smolagents/` and `LICENSE-APACHE` -- All examples (will be re-added per-flow in later PRs) - -Preserved as historical reference: `archive/agent-runtime-final` branch. +Pre-commit hooks (`.pre-commit-config.yaml`) run on push: trailing whitespace, EOF, +ruff, ruff-format, full pytest. Don't bypass hooks unless the user explicitly +authorizes — fix the underlying issue instead. + +## Architecture Principles + +1. **No framework, just lib** — Functions over classes; Protocol over ABC; no plugin + registries or hook discovery +2. **Pure functions** — Flows are `async def run(...)`, not classes; state passed as + args; side effects via explicit hooks +3. **Pydantic at boundaries, frozen dataclass internally** — Pydantic for anything + exposed to LLM (`output_type=`, cfg, input); frozen dataclass for internal value + types +4. **Batch is first-class** — `batch_run(flow_fn, inputs, ...)` will land in PR4 + (concurrency + error handling + progress aggregation). Users do NOT write + `asyncio.gather` boilerplate themselves +5. **Customization 3 layers** — cfg (YAML/CLI), kwargs (Python `extra_*` flow args), + building blocks (fork the flow file). Each layer has explicit extension points +6. **Observability 3 layers** — SDK auto-tracing, external processors via + `add_trace_processor()`, local trajectory archive under `/runs/` +7. **No CLI** — User-facing entry is a runbook script (5 lines of Python), not a + framework command. Magic input is the loose-input UX, resolved by an Agent +8. **Magic input first** — Users describe intent in natural language; + `magic.resolve_magic_input(...)` returns a structured `(input, cfg)` tuple + +## Conventions When Editing + +- **Schemas**: Pydantic, `extra="forbid"`, `frozen=True`. All `KnowledgeItem` + subclasses must require `as_of: datetime` (financial time-sensitivity is mandatory) +- **Configs**: Extend `BaseFlowCfg` (lands in PR2); never use `Dict[str, Any]` in + init signatures +- **Tools**: SDK's `@function_tool` decorator; do NOT subclass anything +- **Memory backends**: Implement the `Memory` Protocol with granular `tools()`, + `mcp_servers()`, `run_hooks()`, `reset()` — each may return an empty list. Do not + force MCP on every implementation +- **Tests**: Subclasses of `unittest.TestCase` in `tests//`. Mock external + dependencies; cover both success and failure paths +- **Imports**: Absolute (`from quantmind.knowledge import Paper`); no relative + imports across module boundaries + +## Things NOT to Do + +- ❌ Rebuild Agent runtime / Tool ABC / lifecycle hook abstraction +- ❌ Add a CLI (`argparse`/`typer`/`click`); users run Python runbook scripts +- ❌ Introduce class-based `BaseFlow` / plugin registry / hook discovery +- ❌ Wrap `from agents import ...` in a QuantMind-side facade — use the SDK directly +- ❌ Mix `batch_run` and `memory` (they will be mutually exclusive in MVP; see PR5) +- ❌ Use `Dict[str, Any]` in init functions; use Pydantic models +- ❌ Add hard deps on observability platforms (Langfuse / Logfire / etc.); document + integration via `add_trace_processor()` in user-facing cookbook only +- ❌ Build embedding-based memory before filesystem memory has shipped and stabilized + +## Reference Material + +- OpenAI Agents SDK docs: +- Lifecycle / RunHooks API: +- MCP integration (filesystem server): +- Tracing (auto-capture, processors, disable): +- Original SDK announcement: +- Removed agent runtime snapshot: `archive/agent-runtime-final` branch on origin + +## Roadmap (post-PR1) + +| PR | Focus | +|----|-------| +| #70 (merged or in review) | Clean removal of self-built agent runtime | +| PR2 | `knowledge/` + `configs/` skeleton | +| PR3 | `preprocess/` (fetch + format two layers) | +| PR4 | `flows/` + `paper_flow` + `batch_run` + `magic.py`; drop old `flow/` `llm/` | +| PR5 | `mind/memory/filesystem` MVP + trajectory archive | +| PR6+ | Second flow (news/earnings) / observability cookbook / longer-term modules | From a568984c4c1d9432ff156267dce851405dda4f16 Mon Sep 17 00:00:00 2001 From: pkuwkl Date: Sat, 25 Apr 2026 23:03:27 +0800 Subject: [PATCH 3/3] docs(claude): require English for PR descriptions and issue bodies Adds a Communication Conventions section to make explicit that PR descriptions, issue bodies, and commit messages must be in English so external readers (search indexers, future maintainers, contributors) can follow the history. Inline review comments / discussion threads can stay in whichever language fits the participants. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 2ba0340..8431d70 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,6 +101,16 @@ authorizes — fix the underlying issue instead. - **Imports**: Absolute (`from quantmind.knowledge import Paper`); no relative imports across module boundaries +## Communication Conventions + +- **PR descriptions and issue bodies must be written in English**, regardless of the + language of the conversation that triggered them. They are read by external audiences + (search indexers, future maintainers, contributors who don't read Chinese). +- Commit messages: English, conventional-commit style (`feat:` / `fix:` / `refactor:` / + `docs:` / `chore:` ...). +- Inline PR review comments and issue discussion threads may be in whichever language + fits the participants. + ## Things NOT to Do - ❌ Rebuild Agent runtime / Tool ABC / lifecycle hook abstraction