From f696b1f751a1810f8261ff3a874456d272606859 Mon Sep 17 00:00:00 2001 From: arahangua Date: Mon, 11 Aug 2025 04:35:23 +0900 Subject: [PATCH 1/2] initial commit: project structure added --- .env.example | 10 + .gitignore | 222 ++------ README.md | 403 +++++++++++++- examples/01_basic_usage.py | 55 ++ examples/02_rag_system.py | 108 ++++ examples/03_persona_interactions.py | 144 +++++ examples/04_document_processing.py | 273 +++++++++ pyproject.toml | 64 +++ src/czero_engine/__init__.py | 41 ++ src/czero_engine/cli.py | 344 ++++++++++++ src/czero_engine/client.py | 520 ++++++++++++++++++ src/czero_engine/models.py | 201 +++++++ src/czero_engine/workflows/__init__.py | 13 + .../workflows/document_processing.py | 507 +++++++++++++++++ src/czero_engine/workflows/knowledge_base.py | 340 ++++++++++++ .../workflows/persona_workflow.py | 437 +++++++++++++++ src/czero_engine/workflows/rag_workflow.py | 357 ++++++++++++ tests/test_integration.py | 225 ++++++++ 18 files changed, 4085 insertions(+), 179 deletions(-) create mode 100644 .env.example create mode 100644 examples/01_basic_usage.py create mode 100644 examples/02_rag_system.py create mode 100644 examples/03_persona_interactions.py create mode 100644 examples/04_document_processing.py create mode 100644 pyproject.toml create mode 100644 src/czero_engine/__init__.py create mode 100644 src/czero_engine/cli.py create mode 100644 src/czero_engine/client.py create mode 100644 src/czero_engine/models.py create mode 100644 src/czero_engine/workflows/__init__.py create mode 100644 src/czero_engine/workflows/document_processing.py create mode 100644 src/czero_engine/workflows/knowledge_base.py create mode 100644 src/czero_engine/workflows/persona_workflow.py create mode 100644 src/czero_engine/workflows/rag_workflow.py create mode 100644 tests/test_integration.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1425e41 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# CZero Engine API Configuration +CZERO_API_URL=http://localhost:1421 + +# Optional settings +CZERO_API_TIMEOUT=30.0 +CZERO_VERBOSE=false + +# Model IDs (optional - will use defaults if not specified) +CZERO_LLM_MODEL_ID= +CZERO_EMBEDDING_MODEL_ID= \ No newline at end of file diff --git a/.gitignore b/.gitignore index b7faf40..3b2da86 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,8 @@ -# Byte-compiled / optimized / DLL files +# Python __pycache__/ -*.py[codz] +*.py[cod] *$py.class - -# C extensions *.so - -# Distribution / packaging .Python build/ develop-eggs/ @@ -20,188 +16,60 @@ parts/ sdist/ var/ wheels/ -share/python-wheels/ *.egg-info/ .installed.cfg *.egg -MANIFEST -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec +# Virtual Environment +.venv/ +venv/ +ENV/ +env/ + +# UV +.uv/ -# Installer logs -pip-log.txt -pip-delete-this-directory.txt +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ -# Unit test / coverage reports +# Testing +.pytest_cache/ +.coverage htmlcov/ .tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover .hypothesis/ -.pytest_cache/ -cover/ -# Translations -*.mo -*.pot +# Environment +.env +.env.local +.env.*.local -# Django stuff: +# Logs *.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation +logs/ + +# OS +.DS_Store +Thumbs.db + +# Project specific +sample_docs/ +sample_project/ +batch_test/ +test_*/ +workspace_*/ +*.db +*.sqlite +*.sqlite3 + +# Documentation build docs/_build/ +docs/.doctrees/ -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock -#poetry.toml - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -#pdm.lock -#pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -#pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - -# Cursor -# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to -# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data -# refer to https://docs.cursor.com/context/ignore-files -.cursorignore -.cursorindexingignore - -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ +# Package management +*.lock +uv.lock \ No newline at end of file diff --git a/README.md b/README.md index 1f2c56f..5a4018d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,401 @@ -# workflow-template -python workflow template for CZero Engine +# CZero Engine Python SDK + +Official Python SDK and workflow templates for CZero Engine API - a powerful document processing and RAG (Retrieval Augmented Generation) system. + +## ๐Ÿš€ Features + +CZero Engine provides: +- **Document Processing**: Extract, chunk, and embed documents (PDFs, text, code, etc.) +- **Vector Search**: Semantic search across your knowledge base +- **RAG System**: Context-aware LLM responses using your documents +- **AI Personas**: Specialized AI assistants (Gestalt, Sage, Pioneer) +- **Workspace Management**: Organize documents into searchable workspaces + +## ๐Ÿ“ฆ Installation + +### Prerequisites +- Python 3.11 or higher +- CZero Engine running locally (port 1421) +- UV package manager (optional but recommended) + +### Install with pip +```bash +pip install czero-engine-python +``` + +### Install from source with UV +```bash +git clone https://github.com/czero/workflow-template.git +cd workflow-template + +# Using UV (recommended) +uv venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +uv pip install -e . + +# Or using pip +python -m venv .venv +source .venv/bin/activate +pip install -e . +``` + +## ๐ŸŽฏ Quick Start + +### 1. Check CZero Engine is Running +```python +import asyncio +from czero_engine import CZeroEngineClient + +async def check_health(): + async with CZeroEngineClient() as client: + health = await client.health_check() + print(f"Status: {health.status}") + print(f"Version: {health.version}") + +asyncio.run(check_health()) +``` + +### 2. Create a Knowledge Base +```python +from czero_engine.workflows import KnowledgeBaseWorkflow + +async def create_kb(): + async with KnowledgeBaseWorkflow() as workflow: + # Process documents into a searchable knowledge base + result = await workflow.create_knowledge_base( + name="My Documentation", + directory_path="./docs", + file_patterns=["*.pdf", "*.md", "*.txt"], + chunk_size=1000, + chunk_overlap=200 + ) + print(f"Processed {result['files_processed']} files") + print(f"Created {result['chunks_created']} chunks") + +asyncio.run(create_kb()) +``` + +### 3. Use RAG for Q&A +```python +from czero_engine.workflows import RAGWorkflow + +async def ask_question(): + async with RAGWorkflow() as workflow: + response = await workflow.ask( + question="What is semantic search and how does it work?", + chunk_limit=5, + similarity_threshold=0.7 + ) + print(response.response) + +asyncio.run(ask_question()) +``` + +## ๐Ÿ“š Workflows + +### Knowledge Base Workflow +Build and query document knowledge bases: + +```python +from czero_engine.workflows import KnowledgeBaseWorkflow + +async with KnowledgeBaseWorkflow() as kb: + # Create knowledge base from documents + await kb.create_knowledge_base( + name="Technical Docs", + directory_path="./documents" + ) + + # Search the knowledge base + results = await kb.query("How does vector search work?") + + # Find similar content + similar = await kb.find_related(chunk_id="chunk_123") + + # Get recommendations + recs = await kb.get_recommendations( + positive_examples=["chunk_1", "chunk_2"] + ) +``` + +### RAG Workflow +Retrieval Augmented Generation for accurate Q&A: + +```python +from czero_engine.workflows import RAGWorkflow + +async with RAGWorkflow() as rag: + # Ask with RAG + response = await rag.ask("Explain document embeddings") + + # Search then ask + result = await rag.search_then_ask( + search_query="vector embeddings", + question="How are they generated?" + ) + + # Compare with/without RAG + comparison = await rag.compare_with_without_rag( + "What is CZero Engine?" + ) +``` + +### Persona Workflow +Interact with specialized AI personas: + +```python +from czero_engine.workflows import PersonaWorkflow + +async with PersonaWorkflow() as personas: + # List available personas + await personas.list_personas() + + # Chat with Gestalt (adaptive assistant) + response = await personas.chat( + "Help me understand RAG systems", + persona_id="gestalt-default" + ) + + # Multi-persona discussion + discussion = await personas.multi_persona_discussion( + topic="Future of AI", + persona_ids=["gestalt-default", "sage", "pioneer"], + rounds=3 + ) +``` + +### Document Processing Workflow +Advanced document processing capabilities: + +```python +from czero_engine.workflows import DocumentProcessingWorkflow + +async with DocumentProcessingWorkflow() as processor: + # Discover files with filtering + files = processor.discover_files( + directory="./docs", + patterns=["*.pdf", "*.md"], + max_size_mb=10 + ) + + # Process documents + stats = await processor.process_documents( + files=files, + workspace_name="Research", + chunk_size=800 + ) + + # Process entire directory tree + await processor.process_directory_tree( + root_directory="./project", + organize_by_type=True + ) +``` + +## ๐Ÿ”ง API Client + +Low-level client for direct API access: + +```python +from czero_engine import CZeroEngineClient + +async with CZeroEngineClient() as client: + # Chat with optional RAG + response = await client.chat( + message="What is CZero Engine?", + use_rag=True, + chunk_limit=5 + ) + + # Semantic search + results = await client.semantic_search( + query="document processing", + limit=10, + similarity_threshold=0.7 + ) + + # Generate embeddings + embedding = await client.generate_embedding( + text="Sample text to embed" + ) + + # Create workspace + workspace = await client.create_workspace( + name="My Workspace", + path="./workspace" + ) + + # Process files + result = await client.process_files( + workspace_id=workspace.id, + files=["doc1.pdf", "doc2.txt"], + chunk_size=1000 + ) +``` + +## ๐Ÿ“‹ CLI Usage + +The SDK includes a CLI for common operations: + +```bash +# Check API health +czero health + +# Create knowledge base +czero create-kb ./documents --name "My KB" --chunk-size 1000 + +# Search +czero search "query text" --limit 10 + +# Ask with RAG +czero ask "Your question here" --use-rag + +# Chat with persona +czero chat --persona gestalt-default + +# Process documents +czero process ./docs --workspace "Research" +``` + +## ๐Ÿ—๏ธ Architecture + +### API Endpoints Used + +The SDK uses these CZero Engine API endpoints: + +- **`POST /api/chat/send`** - LLM text generation with optional RAG +- **`POST /api/vector/search/semantic`** - Semantic search across documents +- **`POST /api/vector/search/similarity`** - Find similar chunks +- **`POST /api/vector/recommendations`** - Get content recommendations +- **`GET /api/documents`** - List all documents +- **`POST /api/embeddings/generate`** - Generate text embeddings +- **`POST /api/workspaces/create`** - Create document workspace +- **`POST /api/workspaces/process`** - Process files into workspace +- **`GET /api/personas/list`** - List available AI personas +- **`POST /api/personas/chat`** - Chat with specific persona +- **`GET /api/health`** - Health check + +### How It Works + +1. **Document Processing**: Documents are extracted, chunked, and converted to vector embeddings +2. **Vector Storage**: Embeddings are stored in a vector database for fast similarity search +3. **RAG Pipeline**: + - User query โ†’ Generate embedding + - Search for similar chunks โ†’ Retrieve context + - Augment prompt with context โ†’ Generate response +4. **Personas**: Specialized system prompts and conversation management + +## ๐Ÿ”ฌ Advanced Examples + +### Building a Q&A System +```python +async def build_qa_system(docs_dir: str): + # Step 1: Create knowledge base + async with KnowledgeBaseWorkflow() as kb: + await kb.create_knowledge_base( + name="QA Knowledge", + directory_path=docs_dir + ) + workspace_id = kb.workspace_id + + # Step 2: Set up RAG for Q&A + async with RAGWorkflow() as rag: + while True: + question = input("Ask a question (or 'quit'): ") + if question.lower() == 'quit': + break + + response = await rag.ask( + question=question, + workspace_filter=workspace_id + ) + print(f"\nAnswer: {response.response}\n") +``` + +### Document Comparison +```python +async def compare_documents(doc1: str, doc2: str): + async with CZeroEngineClient() as client: + # Generate embeddings + emb1 = await client.generate_embedding(doc1) + emb2 = await client.generate_embedding(doc2) + + # Calculate similarity (cosine similarity) + import numpy as np + vec1 = np.array(emb1.embedding) + vec2 = np.array(emb2.embedding) + + similarity = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) + print(f"Document similarity: {similarity:.3f}") +``` + +### Batch Processing with Progress +```python +from pathlib import Path + +async def batch_process_with_progress(root_dir: str): + async with DocumentProcessingWorkflow(verbose=True) as processor: + # Discover all documents + files = processor.discover_files( + directory=root_dir, + patterns=["*.pdf", "*.docx", "*.txt"] + ) + + print(f"Found {len(files)} files to process") + + # Process in batches of 20 + batch_size = 20 + for i in range(0, len(files), batch_size): + batch = files[i:i+batch_size] + print(f"\nProcessing batch {i//batch_size + 1}...") + + stats = await processor.process_documents( + files=batch, + workspace_name=f"Batch_{i//batch_size + 1}", + chunk_size=1000 + ) + + print(f"Success rate: {stats.success_rate:.1f}%") +``` + +## ๐Ÿ” Environment Configuration + +Create a `.env` file: + +```env +# CZero Engine API Configuration +CZERO_API_URL=http://localhost:1421 + +# Optional settings +CZERO_API_TIMEOUT=30.0 +CZERO_VERBOSE=true +``` + +## ๐Ÿค Contributing + +Contributions are welcome! Please: +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## ๐Ÿ“œ License + +MIT License - see LICENSE file for details + +## ๐Ÿ“ง Support + +- Email: info@czero.cc +- Documentation: https://docs.czero.cc +- Issues: https://github.com/czero/workflow-template/issues + +## ๐Ÿ”— Related + +- [CZero Engine](https://github.com/czero/czero-engine) - Main engine repository +- [CZero Overlay](https://github.com/czero/czero-overlay) - Desktop application +- [API Documentation](https://api.czero.cc/docs) - Full API reference + +--- + +Built with โค๏ธ by the CZero Team \ No newline at end of file diff --git a/examples/01_basic_usage.py b/examples/01_basic_usage.py new file mode 100644 index 0000000..e49bf77 --- /dev/null +++ b/examples/01_basic_usage.py @@ -0,0 +1,55 @@ +"""Basic usage examples for CZero Engine Python SDK.""" + +import asyncio +from czero_engine import CZeroEngineClient + + +async def basic_examples(): + """Demonstrate basic SDK usage.""" + + # Initialize client + async with CZeroEngineClient() as client: + + # 1. Health check + print("1. Checking API health...") + health = await client.health_check() + print(f" Status: {health['status']}") + print() + + # 2. Simple chat without RAG + print("2. Chat without RAG...") + response = await client.chat( + message="What is machine learning?", + use_rag=False + ) + print(f" Response: {response.response[:200]}...") + print() + + # 3. Create a workspace + print("3. Creating workspace...") + workspace = await client.create_workspace( + name="Example Workspace", + path="./documents", + description="Test workspace for examples" + ) + print(f" Workspace ID: {workspace.id}") + print() + + # 4. Generate embedding + print("4. Generating embedding...") + embedding = await client.generate_embedding( + text="CZero Engine is a powerful document processing system" + ) + print(f" Embedding dimensions: {len(embedding.embedding)}") + print(f" First 5 values: {embedding.embedding[:5]}") + print() + + # 5. List personas + print("5. Available personas...") + personas = await client.list_personas() + for persona in personas.personas: + print(f" - {persona.name}: {persona.specialty}") + + +if __name__ == "__main__": + asyncio.run(basic_examples()) \ No newline at end of file diff --git a/examples/02_rag_system.py b/examples/02_rag_system.py new file mode 100644 index 0000000..7a99cd5 --- /dev/null +++ b/examples/02_rag_system.py @@ -0,0 +1,108 @@ +"""RAG (Retrieval Augmented Generation) system example.""" + +import asyncio +from pathlib import Path +from czero_engine.workflows import KnowledgeBaseWorkflow, RAGWorkflow + + +async def rag_example(): + """Build and use a RAG system.""" + + # Step 1: Create knowledge base from documents + print("Step 1: Creating knowledge base...") + async with KnowledgeBaseWorkflow() as kb_workflow: + + # Ensure we have a documents directory + docs_dir = Path("./sample_docs") + docs_dir.mkdir(exist_ok=True) + + # Create some sample documents + (docs_dir / "ai_basics.txt").write_text(""" + Artificial Intelligence (AI) refers to the simulation of human intelligence + in machines. Machine learning is a subset of AI that enables systems to + learn from data. Deep learning uses neural networks with multiple layers + to process complex patterns. Natural language processing helps computers + understand human language. + """) + + (docs_dir / "czero_engine.md").write_text(""" + # CZero Engine Overview + + CZero Engine is a comprehensive document processing and RAG system. + It provides: + - Document extraction and chunking + - Vector embeddings for semantic search + - Integration with multiple LLM backends + - AI personas for specialized interactions + - Workspace management for organizing documents + + The system uses ONNX Runtime for efficient model inference and supports + GPU acceleration for faster processing. + """) + + (docs_dir / "semantic_search.txt").write_text(""" + Semantic search goes beyond keyword matching to understand the meaning + and intent behind queries. It uses vector embeddings to represent text + as high-dimensional vectors. Similar content has vectors that are close + together in the vector space. This enables finding relevant information + even when exact keywords don't match. + """) + + # Create knowledge base + result = await kb_workflow.create_knowledge_base( + name="AI Documentation", + directory_path=str(docs_dir), + file_patterns=["*.txt", "*.md"], + chunk_size=500, + chunk_overlap=50 + ) + + print(f" Created workspace: {result['workspace_id']}") + print(f" Processed {result['files_processed']} files") + print(f" Created {result['chunks_created']} chunks") + print() + + # Step 2: Use RAG for Q&A + print("Step 2: Using RAG for questions...") + async with RAGWorkflow() as rag_workflow: + + # Ask questions with RAG + questions = [ + "What is CZero Engine and what are its main features?", + "How does semantic search work?", + "What's the difference between AI, machine learning, and deep learning?", + "Does CZero Engine support GPU acceleration?" + ] + + for i, question in enumerate(questions, 1): + print(f"\nQ{i}: {question}") + response = await rag_workflow.ask( + question=question, + chunk_limit=3, + similarity_threshold=0.6 + ) + print(f"A{i}: {response.response[:300]}...") + + if response.context_used: + print(f" (Used {len(response.context_used)} context chunks)") + + print("\n" + "="*50) + + # Compare with and without RAG + print("\nStep 3: Comparing with/without RAG...") + comparison_q = "What document processing features does CZero Engine provide?" + + comparison = await rag_workflow.compare_with_without_rag( + question=comparison_q + ) + + print(f"\nQuestion: {comparison_q}") + print("\nWithout RAG:") + print(f" {comparison['without_rag'][:200]}...") + print("\nWith RAG:") + print(f" {comparison['with_rag'][:200]}...") + print(f"\n Context chunks used: {comparison['chunks_used']}") + + +if __name__ == "__main__": + asyncio.run(rag_example()) \ No newline at end of file diff --git a/examples/03_persona_interactions.py b/examples/03_persona_interactions.py new file mode 100644 index 0000000..66abc9d --- /dev/null +++ b/examples/03_persona_interactions.py @@ -0,0 +1,144 @@ +"""Examples of interacting with AI personas.""" + +import asyncio +from czero_engine.workflows import PersonaWorkflow + + +async def persona_examples(): + """Demonstrate persona interactions.""" + + async with PersonaWorkflow() as workflow: + + # 1. List available personas + print("1. Available Personas:") + print("="*50) + await workflow.list_personas() + print() + + # 2. Chat with different personas + print("2. Individual Persona Chats:") + print("="*50) + + # Chat with Gestalt + print("\n--- Gestalt (Adaptive Assistant) ---") + await workflow.select_persona("gestalt-default") + + response = await workflow.chat( + "Hello! Can you introduce yourself and explain what makes you unique?" + ) + + # Continue conversation + await workflow.chat( + "How would you help someone learn about AI and machine learning?" + ) + + # Chat with Sage + print("\n--- Sage (Research & Analysis) ---") + await workflow.select_persona("sage") + + await workflow.chat( + "What are the philosophical implications of AGI (Artificial General Intelligence)?" + ) + + # Chat with Pioneer + print("\n--- Pioneer (Innovation) ---") + await workflow.select_persona("pioneer") + + await workflow.chat( + "What innovative applications could combine AR/VR with AI?" + ) + + # 3. Multi-persona discussion + print("\n3. Multi-Persona Discussion:") + print("="*50) + + discussion = await workflow.multi_persona_discussion( + topic="The role of AI in education: opportunities and challenges", + persona_ids=["gestalt-default", "sage", "pioneer"], + rounds=2 + ) + + print("\nDiscussion Summary:") + for entry in discussion: + print(f"\nRound {entry['round']} - {entry['persona']}:") + print(f" {entry['response'][:200]}...") + + # 4. Persona comparison on same question + print("\n4. Persona Comparison:") + print("="*50) + + question = "How should we approach the ethics of AI development?" + print(f"\nQuestion: {question}\n") + + responses = await workflow.persona_comparison( + question=question, + persona_ids=["gestalt-default", "sage", "pioneer"] + ) + + for persona_id, response in responses.items(): + print(f"\n{persona_id}:") + print(f" {response.response[:250]}...") + + # 5. Get conversation summary + print("\n5. Conversation Summary:") + print("="*50) + + # Switch back to Gestalt to check conversation history + await workflow.select_persona("gestalt-default") + summary = workflow.get_conversation_summary() + + print(f"\nActive persona: {summary['persona']}") + print(f"Total turns: {summary['turn_count']}") + print(f"Message count: {summary['message_count']}") + + if summary['recent_messages']: + print("\nRecent messages:") + for msg in summary['recent_messages'][-4:]: + role = msg['role'].capitalize() + content = msg['content'][:100] + "..." if len(msg['content']) > 100 else msg['content'] + print(f" {role}: {content}") + + +async def interactive_persona_chat(): + """Interactive chat example with a persona.""" + + async with PersonaWorkflow() as workflow: + print("Starting interactive chat with Gestalt...") + print("="*50) + + await workflow.select_persona("gestalt-default") + + # Simulate a conversation + messages = [ + "Hello! I'm interested in learning about vector databases.", + "What makes them different from traditional databases?", + "Can you give me a practical example of when to use one?", + "How do they relate to RAG systems?", + "Thank you for the explanation!" + ] + + for message in messages: + print(f"\nYou: {message}") + response = await workflow.chat( + message=message, + maintain_history=True + ) + # Response is printed by the workflow if verbose=True + + # Small delay to simulate conversation flow + await asyncio.sleep(0.5) + + # Show final conversation summary + print("\n" + "="*50) + summary = workflow.get_conversation_summary() + print(f"Conversation ended with {summary['turn_count']} turns") + + +if __name__ == "__main__": + print("Running persona examples...") + asyncio.run(persona_examples()) + + print("\n\n" + "="*70) + print("Running interactive chat example...") + print("="*70) + asyncio.run(interactive_persona_chat()) \ No newline at end of file diff --git a/examples/04_document_processing.py b/examples/04_document_processing.py new file mode 100644 index 0000000..2a7723a --- /dev/null +++ b/examples/04_document_processing.py @@ -0,0 +1,273 @@ +"""Advanced document processing examples.""" + +import asyncio +from pathlib import Path +from czero_engine.workflows import DocumentProcessingWorkflow + + +async def document_processing_example(): + """Demonstrate document processing capabilities.""" + + # Create sample project structure + print("Setting up sample project structure...") + project_root = Path("./sample_project") + + # Create directories + (project_root / "src").mkdir(parents=True, exist_ok=True) + (project_root / "docs").mkdir(parents=True, exist_ok=True) + (project_root / "tests").mkdir(parents=True, exist_ok=True) + (project_root / "data").mkdir(parents=True, exist_ok=True) + + # Create sample files + (project_root / "README.md").write_text(""" + # Sample Project + + This is a sample project for demonstrating CZero Engine's document processing. + + ## Features + - Document extraction + - Text chunking + - Vector embeddings + - Semantic search + """) + + (project_root / "src" / "main.py").write_text(""" + # Main application file + + def process_documents(path): + '''Process documents in the given path.''' + print(f"Processing documents in {path}") + # Implementation here + return True + + def search(query, limit=10): + '''Search for documents matching the query.''' + results = [] + # Search implementation + return results + """) + + (project_root / "src" / "utils.py").write_text(""" + # Utility functions + + def chunk_text(text, chunk_size=1000, overlap=200): + '''Split text into overlapping chunks.''' + chunks = [] + start = 0 + while start < len(text): + end = start + chunk_size + chunks.append(text[start:end]) + start = end - overlap + return chunks + + def calculate_similarity(vec1, vec2): + '''Calculate cosine similarity between vectors.''' + # Similarity calculation + return 0.95 + """) + + (project_root / "docs" / "api.md").write_text(""" + # API Documentation + + ## Endpoints + + ### POST /api/process + Process documents and create embeddings. + + ### GET /api/search + Search for similar documents. + + ### POST /api/chat + Chat with AI using document context. + """) + + (project_root / "docs" / "architecture.txt").write_text(""" + System Architecture + + The system consists of three main components: + 1. Document Processor - Extracts and chunks text + 2. Embedding Service - Generates vector embeddings + 3. Search Engine - Performs semantic search + + Data flows from documents through the processor to the embedding service, + and finally into the vector database for searching. + """) + + (project_root / "tests" / "test_main.py").write_text(""" + import unittest + from src.main import process_documents, search + + class TestDocumentProcessing(unittest.TestCase): + def test_process_documents(self): + result = process_documents("./test_data") + self.assertTrue(result) + + def test_search(self): + results = search("test query") + self.assertIsInstance(results, list) + """) + + print("Sample project structure created.\n") + + # Process documents + async with DocumentProcessingWorkflow(verbose=True) as workflow: + + # 1. Discover files with filtering + print("\n1. File Discovery:") + print("="*50) + + all_files = workflow.discover_files( + directory=str(project_root), + patterns=["*.py", "*.md", "*.txt"], + max_size_mb=10 + ) + + print(f"\nFound {len(all_files)} files total") + + # 2. Process specific file types + print("\n2. Processing Python Files:") + print("="*50) + + python_files = [f for f in all_files if f.suffix == ".py"] + + if python_files: + stats = await workflow.process_documents( + files=python_files, + workspace_name="Python Code", + chunk_size=500, + chunk_overlap=100, + batch_size=5 + ) + + print(f"\nPython files processed: {stats.processed_files}") + print(f"Success rate: {stats.success_rate:.1f}%") + + # 3. Process documentation files + print("\n3. Processing Documentation:") + print("="*50) + + doc_files = [f for f in all_files if f.suffix in [".md", ".txt"]] + + if doc_files: + stats = await workflow.process_documents( + files=doc_files, + workspace_name="Documentation", + chunk_size=800, + chunk_overlap=200 + ) + + print(f"\nDoc files processed: {stats.processed_files}") + print(f"Chunks created: {stats.total_chunks}") + + # 4. Process entire directory tree with organization + print("\n4. Processing Directory Tree (Organized by Type):") + print("="*50) + + workspace_stats = await workflow.process_directory_tree( + root_directory=str(project_root), + workspace_prefix="organized", + organize_by_type=True, + chunk_size=600, + batch_size=3 + ) + + print("\nWorkspace Summary:") + total_processed = sum(s.processed_files for s in workspace_stats.values()) + total_chunks = sum(s.total_chunks for s in workspace_stats.values()) + print(f" Total workspaces created: {len(workspace_stats)}") + print(f" Total files processed: {total_processed}") + print(f" Total chunks created: {total_chunks}") + + # 5. Generate embeddings for custom content + print("\n5. Custom Embedding Generation:") + print("="*50) + + custom_texts = [ + "CZero Engine provides powerful document processing capabilities", + "Vector embeddings enable semantic understanding of text", + "RAG systems combine retrieval with generation for accurate responses" + ] + + embeddings = await workflow.generate_embeddings_for_text(custom_texts) + + print(f"\nGenerated {len(embeddings)} embeddings") + for i, (text, emb) in enumerate(zip(custom_texts, embeddings), 1): + print(f" {i}. Text: '{text[:50]}...'") + print(f" Dimensions: {len(emb.embedding)}") + + +async def batch_processing_example(): + """Example of batch processing with progress tracking.""" + + print("\nBatch Processing Example:") + print("="*70) + + # Create a larger set of test files + test_dir = Path("./batch_test") + test_dir.mkdir(exist_ok=True) + + # Generate multiple test files + for i in range(25): + file_path = test_dir / f"document_{i:03d}.txt" + content = f""" + Document {i} + + This is test document number {i}. It contains sample text for processing. + The document discusses various topics related to AI and machine learning. + + Key concepts covered: + - Neural networks and deep learning + - Natural language processing + - Computer vision applications + - Reinforcement learning strategies + + Each document is unique but shares common themes to test the processing + and chunking capabilities of the system. + """ + file_path.write_text(content * 3) # Make files larger + + print(f"Created 25 test documents in {test_dir}") + + async with DocumentProcessingWorkflow(verbose=True) as workflow: + + # Discover all files + files = workflow.discover_files( + directory=str(test_dir), + patterns=["*.txt"] + ) + + print(f"\nProcessing {len(files)} files in batches...") + + # Process in batches with progress + stats = await workflow.process_documents( + files=files, + workspace_name="Batch Processing Test", + batch_size=5, # Process 5 files at a time + chunk_size=300, + chunk_overlap=50 + ) + + print("\nBatch Processing Results:") + print(f" Total files: {stats.total_files}") + print(f" Successfully processed: {stats.processed_files}") + print(f" Failed: {stats.failed_files}") + print(f" Success rate: {stats.success_rate:.1f}%") + print(f" Total chunks: {stats.total_chunks}") + print(f" Processing time: {stats.processing_time:.2f} seconds") + + if stats.processing_time > 0: + throughput = stats.total_size_bytes / (1024 * 1024) / stats.processing_time + print(f" Throughput: {throughput:.2f} MB/s") + + # Cleanup + import shutil + shutil.rmtree(test_dir) + print(f"\nCleaned up test directory: {test_dir}") + + +if __name__ == "__main__": + print("Running document processing examples...") + asyncio.run(document_processing_example()) + + print("\n\n" + "="*70) + asyncio.run(batch_processing_example()) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b5c0c72 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +[project] +name = "czero-engine-python" +version = "0.1.0" +description = "Official Python SDK and workflows for CZero Engine API" +readme = "README.md" +requires-python = ">=3.11" +license = {text = "MIT"} +authors = [ + {name = "CZero Engine Team", email = "info@czero.cc"}, +] +keywords = ["czero", "czero-engine", "rag", "llm", "vector-search", "document-processing", "ai", "workflow"] + +dependencies = [ + # Core dependencies + "httpx>=0.27.0", # Modern async HTTP client + "pydantic>=2.9.0", # Data validation and models + "python-dotenv>=1.0.0", # Environment management + "rich>=13.9.0", # Beautiful terminal output + "typer>=0.12.0", # Modern CLI framework + + # Utilities + "pathlib>=1.0.1", + "asyncio>=3.4.3", +] + +[project.optional-dependencies] +dev = [ + "black>=24.10.0", + "ruff>=0.7.0", + "mypy>=1.13.0", + "ipython>=8.29.0", +] + +[project.scripts] +czero = "czero_engine.cli:app" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.uv] +dev-dependencies = [ + "pytest>=8.3.3", + "pytest-asyncio>=0.24.0", + "pytest-cov>=6.0.0", +] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W", "B", "C90", "D"] +ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107"] + +[tool.black] +line-length = 100 +target-version = ['py311'] + +[tool.mypy] +python_version = "3.11" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true \ No newline at end of file diff --git a/src/czero_engine/__init__.py b/src/czero_engine/__init__.py new file mode 100644 index 0000000..b2d22cb --- /dev/null +++ b/src/czero_engine/__init__.py @@ -0,0 +1,41 @@ +"""CZero Engine Python Client - Official Python SDK for CZero Engine API.""" + +__version__ = "0.1.0" + +from .client import CZeroEngineClient +from .models import ( + ChatRequest, + ChatResponse, + SemanticSearchRequest, + SemanticSearchResponse, + WorkspaceCreateRequest, + WorkspaceResponse, + ProcessFilesRequest, + ProcessFilesResponse, + PersonaChatRequest, + PersonaChatResponse, +) +from .workflows import ( + KnowledgeBaseWorkflow, + RAGWorkflow, + PersonaWorkflow, + DocumentProcessingWorkflow, +) + +__all__ = [ + "CZeroEngineClient", + "ChatRequest", + "ChatResponse", + "SemanticSearchRequest", + "SemanticSearchResponse", + "WorkspaceCreateRequest", + "WorkspaceResponse", + "ProcessFilesRequest", + "ProcessFilesResponse", + "PersonaChatRequest", + "PersonaChatResponse", + "KnowledgeBaseWorkflow", + "RAGWorkflow", + "PersonaWorkflow", + "DocumentProcessingWorkflow", +] \ No newline at end of file diff --git a/src/czero_engine/cli.py b/src/czero_engine/cli.py new file mode 100644 index 0000000..7b46040 --- /dev/null +++ b/src/czero_engine/cli.py @@ -0,0 +1,344 @@ +"""CLI for CZero Engine Python SDK.""" + +import asyncio +from pathlib import Path +from typing import Optional +import typer +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +from rich.progress import Progress, SpinnerColumn, TextColumn +import json + +from .client import CZeroEngineClient +from .workflows import ( + KnowledgeBaseWorkflow, + RAGWorkflow, + PersonaWorkflow, + DocumentProcessingWorkflow +) + +app = typer.Typer(help="CZero Engine CLI - Interact with CZero Engine API") +console = Console() + + +@app.command() +def health(): + """Check CZero Engine API health status.""" + async def check(): + async with CZeroEngineClient() as client: + try: + result = await client.health_check() + console.print(Panel( + f"[green]โœ“[/green] CZero Engine API is healthy\n" + f"Status: {result['status']}\n" + f"Version: {result.get('version', 'Unknown')}\n" + f"Service: {result.get('service', 'czero-api')}", + title="Health Check", + expand=False + )) + except Exception as e: + console.print(f"[red]โœ— API health check failed: {e}[/red]") + + asyncio.run(check()) + + +@app.command("create-kb") +def create_knowledge_base( + directory: str = typer.Argument(..., help="Directory containing documents"), + name: str = typer.Option("Knowledge Base", "--name", "-n", help="Workspace name"), + chunk_size: int = typer.Option(1000, "--chunk-size", "-c", help="Chunk size"), + chunk_overlap: int = typer.Option(200, "--overlap", "-o", help="Chunk overlap"), + patterns: Optional[str] = typer.Option(None, "--patterns", "-p", help="File patterns (comma-separated)") +): + """Create a knowledge base from documents.""" + async def create(): + async with KnowledgeBaseWorkflow() as workflow: + file_patterns = patterns.split(",") if patterns else None + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + progress.add_task(f"Creating knowledge base from {directory}...", total=None) + + try: + result = await workflow.create_knowledge_base( + name=name, + directory_path=directory, + file_patterns=file_patterns, + chunk_size=chunk_size, + chunk_overlap=chunk_overlap + ) + + console.print(f"[green]โœ“[/green] Knowledge base created successfully") + console.print(f" Workspace ID: {result['workspace_id']}") + console.print(f" Files processed: {result['files_processed']}") + console.print(f" Chunks created: {result['chunks_created']}") + + except Exception as e: + console.print(f"[red]โœ— Failed to create knowledge base: {e}[/red]") + + asyncio.run(create()) + + +@app.command() +def search( + query: str = typer.Argument(..., help="Search query"), + limit: int = typer.Option(10, "--limit", "-l", help="Number of results"), + threshold: float = typer.Option(0.7, "--threshold", "-t", help="Similarity threshold"), + workspace: Optional[str] = typer.Option(None, "--workspace", "-w", help="Workspace filter") +): + """Semantic search across documents.""" + async def run_search(): + async with CZeroEngineClient() as client: + try: + results = await client.semantic_search( + query=query, + limit=limit, + similarity_threshold=threshold, + workspace_filter=workspace + ) + + if results.results: + table = Table(title=f"Search Results for: {query}") + table.add_column("Score", style="cyan", width=10) + table.add_column("Document", style="green") + table.add_column("Content", style="white", overflow="fold") + + for result in results.results: + table.add_row( + f"{result.similarity:.3f}", + result.document_id[:20] + "..." if len(result.document_id) > 20 else result.document_id, + result.content[:100] + "..." if len(result.content) > 100 else result.content + ) + + console.print(table) + else: + console.print("[yellow]No results found[/yellow]") + + except Exception as e: + console.print(f"[red]โœ— Search failed: {e}[/red]") + + asyncio.run(run_search()) + + +@app.command() +def ask( + question: str = typer.Argument(..., help="Question to ask"), + use_rag: bool = typer.Option(True, "--rag/--no-rag", help="Use RAG for context"), + chunks: int = typer.Option(5, "--chunks", "-c", help="Number of context chunks"), + model: Optional[str] = typer.Option(None, "--model", "-m", help="Model ID to use") +): + """Ask a question using LLM with optional RAG.""" + async def run_ask(): + async with CZeroEngineClient() as client: + try: + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + progress.add_task("Generating response...", total=None) + + response = await client.chat( + message=question, + use_rag=use_rag, + chunk_limit=chunks if use_rag else None, + model_id=model + ) + + console.print(Panel( + response.response, + title="Response", + expand=False + )) + + if use_rag and response.context_used: + console.print("\n[dim]Context sources used:[/dim]") + for ctx in response.context_used[:3]: + console.print(f" โ€ข [dim]{ctx.chunk_id[:40]}... (similarity: {ctx.similarity:.3f})[/dim]") + + except Exception as e: + console.print(f"[red]โœ— Failed to generate response: {e}[/red]") + + asyncio.run(run_ask()) + + +@app.command() +def chat( + persona: str = typer.Option("gestalt-default", "--persona", "-p", help="Persona ID"), + model: Optional[str] = typer.Option(None, "--model", "-m", help="Model ID to use") +): + """Interactive chat with a persona.""" + async def run_chat(): + async with PersonaWorkflow() as workflow: + # Select persona + await workflow.select_persona(persona) + + console.print("[cyan]Interactive chat started. Type 'exit' to quit.[/cyan]\n") + + while True: + try: + message = typer.prompt("You") + + if message.lower() in ["exit", "quit", "bye"]: + console.print("[yellow]Goodbye![/yellow]") + break + + # Get response + await workflow.chat( + message=message, + model_id=model, + maintain_history=True + ) + + except KeyboardInterrupt: + console.print("\n[yellow]Chat interrupted[/yellow]") + break + except Exception as e: + console.print(f"[red]Error: {e}[/red]") + + asyncio.run(run_chat()) + + +@app.command() +def process( + directory: str = typer.Argument(..., help="Directory to process"), + workspace: str = typer.Option("default", "--workspace", "-w", help="Workspace name"), + batch_size: int = typer.Option(10, "--batch", "-b", help="Batch size"), + chunk_size: int = typer.Option(1000, "--chunk-size", "-c", help="Chunk size") +): + """Process documents in a directory.""" + async def run_process(): + async with DocumentProcessingWorkflow() as workflow: + # Discover files + files = workflow.discover_files(directory) + + if not files: + console.print("[yellow]No files found to process[/yellow]") + return + + console.print(f"[cyan]Found {len(files)} files to process[/cyan]") + + # Process files + stats = await workflow.process_documents( + files=files, + workspace_name=workspace, + batch_size=batch_size, + chunk_size=chunk_size + ) + + # Show results + console.print(f"\n[green]Processing complete![/green]") + console.print(f" Success rate: {stats.success_rate:.1f}%") + console.print(f" Files processed: {stats.processed_files}/{stats.total_files}") + console.print(f" Chunks created: {stats.total_chunks}") + console.print(f" Time taken: {stats.processing_time:.2f}s") + + asyncio.run(run_process()) + + +@app.command() +def personas(): + """List available personas.""" + async def list_personas(): + async with PersonaWorkflow() as workflow: + await workflow.list_personas() + + asyncio.run(list_personas()) + + +@app.command() +def documents(): + """List all documents.""" + async def list_docs(): + async with CZeroEngineClient() as client: + try: + result = await client.list_documents() + + if result.documents: + table = Table(title="Documents") + table.add_column("ID", style="cyan", overflow="fold") + table.add_column("Title", style="green") + table.add_column("Type", style="yellow") + table.add_column("Size", style="magenta") + table.add_column("Workspace", style="blue") + + for doc in result.documents[:20]: # Show first 20 + size_mb = doc.size / (1024 * 1024) + table.add_row( + doc.id[:12] + "...", + doc.title[:30] + "..." if len(doc.title) > 30 else doc.title, + doc.content_type or "unknown", + f"{size_mb:.2f} MB", + doc.workspace_id[:8] + "..." if doc.workspace_id else "" + ) + + console.print(table) + + if len(result.documents) > 20: + console.print(f"\n[dim]Showing 20 of {len(result.documents)} documents[/dim]") + else: + console.print("[yellow]No documents found[/yellow]") + + except Exception as e: + console.print(f"[red]โœ— Failed to list documents: {e}[/red]") + + asyncio.run(list_docs()) + + +@app.command() +def embed( + text: str = typer.Argument(..., help="Text to generate embedding for"), + model: Optional[str] = typer.Option(None, "--model", "-m", help="Model ID to use"), + output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file for embedding") +): + """Generate embedding for text.""" + async def generate(): + async with CZeroEngineClient() as client: + try: + result = await client.generate_embedding( + text=text, + model_id=model + ) + + console.print(f"[green]โœ“[/green] Embedding generated") + console.print(f" Model: {result.model_used}") + console.print(f" Dimensions: {len(result.embedding)}") + + if output: + # Save to file + output_data = { + "text": text, + "model": result.model_used, + "embedding": result.embedding + } + Path(output).write_text(json.dumps(output_data, indent=2)) + console.print(f" Saved to: {output}") + else: + # Show first few dimensions + console.print(f" First 10 values: {result.embedding[:10]}") + + except Exception as e: + console.print(f"[red]โœ— Failed to generate embedding: {e}[/red]") + + asyncio.run(generate()) + + +@app.command() +def version(): + """Show version information.""" + console.print(Panel( + "[bold cyan]CZero Engine Python SDK[/bold cyan]\n" + "Version: 0.1.0\n" + "Python: 3.11+\n" + "API Endpoint: http://localhost:1421", + title="Version Info", + expand=False + )) + + +if __name__ == "__main__": + app() \ No newline at end of file diff --git a/src/czero_engine/client.py b/src/czero_engine/client.py new file mode 100644 index 0000000..60eedca --- /dev/null +++ b/src/czero_engine/client.py @@ -0,0 +1,520 @@ +"""Official Python client for CZero Engine API.""" + +import asyncio +from typing import Any, Dict, List, Optional, Union +import httpx +from rich.console import Console +from rich.table import Table +from rich.panel import Panel +import json + +from .models import ( + ChatRequest, ChatResponse, + SemanticSearchRequest, SemanticSearchResponse, + SimilaritySearchRequest, RecommendationsRequest, + DocumentsResponse, DocumentMetadata, + EmbeddingRequest, EmbeddingResponse, + WorkspaceCreateRequest, WorkspaceResponse, + ProcessFilesRequest, ProcessFilesResponse, ProcessingConfig, + PersonaListResponse, PersonaChatRequest, PersonaChatResponse, + HealthResponse, +) + +console = Console() + + +class CZeroEngineClient: + """ + Official Python client for CZero Engine API. + + CZero Engine provides: + - LLM text generation with RAG (Retrieval Augmented Generation) + - Semantic vector search across documents + - Document processing and workspace management + - Embedding generation for text + - AI personas for specialized interactions + + All methods are async and return typed responses. + """ + + def __init__( + self, + base_url: str = "http://localhost:1421", + timeout: float = 30.0, + verbose: bool = False + ): + """ + Initialize CZero Engine client. + + Args: + base_url: Base URL for CZero Engine API (default: http://localhost:1421) + timeout: Request timeout in seconds + verbose: Enable verbose logging + """ + self.base_url = base_url.rstrip("/") + self.timeout = timeout + self.verbose = verbose + self.client = httpx.AsyncClient(timeout=timeout) + + async def __aenter__(self): + """Async context manager entry.""" + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + await self.close() + + async def close(self): + """Close the HTTP client.""" + await self.client.aclose() + + def _log(self, message: str): + """Log message if verbose mode is enabled.""" + if self.verbose: + console.print(f"[dim]{message}[/dim]") + + # ==================== Health Check ==================== + + async def health_check(self) -> HealthResponse: + """ + Check if CZero Engine API is healthy. + + Returns: + HealthResponse with service status + """ + self._log("Checking API health...") + response = await self.client.get(f"{self.base_url}/api/health") + response.raise_for_status() + return HealthResponse(**response.json()) + + # ==================== Chat/LLM Endpoints ==================== + + async def chat( + self, + message: str, + use_rag: bool = True, + model_id: Optional[str] = None, + system_prompt: Optional[str] = None, + max_tokens: int = 1024, + temperature: float = 0.7, + similarity_threshold: float = 0.7, + chunk_limit: int = 5, + use_web_search: bool = False + ) -> ChatResponse: + """ + Send a chat message to CZero Engine LLM with optional RAG. + + This endpoint combines LLM generation with semantic search to provide + context-aware responses based on your document knowledge base. + + Args: + message: The user message/prompt + use_rag: Whether to use RAG for context retrieval + model_id: Optional specific model ID to use + system_prompt: Optional system prompt to guide the response + max_tokens: Maximum tokens to generate + temperature: Temperature for generation (0.0-1.0) + similarity_threshold: Minimum similarity for RAG chunks + chunk_limit: Maximum number of context chunks to retrieve + use_web_search: Whether to enable web search (if available) + + Returns: + ChatResponse with generated text and optional context chunks + """ + request = ChatRequest( + message=message, + model_id=model_id, + use_rag=use_rag, + rag_config={ + "similarity_threshold": similarity_threshold, + "chunk_limit": chunk_limit + } if use_rag else None, + use_web_search=use_web_search, + system_prompt=system_prompt, + max_tokens=max_tokens, + temperature=temperature + ) + + self._log(f"Sending chat request (RAG: {use_rag})...") + response = await self.client.post( + f"{self.base_url}/api/chat/send", + json=request.model_dump(exclude_none=True) + ) + response.raise_for_status() + return ChatResponse(**response.json()) + + # ==================== Vector Search Endpoints ==================== + + async def semantic_search( + self, + query: str, + limit: int = 10, + similarity_threshold: float = 0.7, + include_content: bool = True, + workspace_filter: Optional[str] = None + ) -> SemanticSearchResponse: + """ + Perform semantic search across your document knowledge base. + + Uses vector embeddings to find semantically similar content, + not just keyword matches. + + Args: + query: Search query text + limit: Maximum number of results to return + similarity_threshold: Minimum similarity score (0.0-1.0) + include_content: Whether to include full content in results + workspace_filter: Optional workspace ID to limit search + + Returns: + SemanticSearchResponse with matching chunks + """ + request = SemanticSearchRequest( + query=query, + limit=limit, + similarity_threshold=similarity_threshold, + include_content=include_content, + workspace_filter=workspace_filter + ) + + self._log(f"Searching for: {query[:50]}...") + response = await self.client.post( + f"{self.base_url}/api/vector/search/semantic", + json=request.model_dump(exclude_none=True) + ) + response.raise_for_status() + return SemanticSearchResponse(**response.json()) + + async def find_similar_chunks( + self, + chunk_id: str, + limit: int = 5, + similarity_threshold: float = 0.5 + ) -> SemanticSearchResponse: + """ + Find chunks similar to a given chunk ID. + + Useful for finding related content or duplicates. + + Args: + chunk_id: ID of the reference chunk + limit: Maximum number of results + similarity_threshold: Minimum similarity score + + Returns: + SemanticSearchResponse with similar chunks + """ + request = SimilaritySearchRequest( + chunk_id=chunk_id, + limit=limit, + similarity_threshold=similarity_threshold + ) + + self._log(f"Finding similar to chunk: {chunk_id}") + response = await self.client.post( + f"{self.base_url}/api/vector/search/similarity", + json=request.model_dump() + ) + response.raise_for_status() + return SemanticSearchResponse(**response.json()) + + async def get_recommendations( + self, + positive_chunk_ids: List[str], + negative_chunk_ids: Optional[List[str]] = None, + limit: int = 10 + ) -> SemanticSearchResponse: + """ + Get content recommendations based on positive/negative examples. + + Uses vector math to find content similar to positive examples + and dissimilar to negative examples. + + Args: + positive_chunk_ids: Chunk IDs to find similar content to + negative_chunk_ids: Chunk IDs to avoid similarity to + limit: Maximum number of recommendations + + Returns: + SemanticSearchResponse with recommended chunks + """ + request = RecommendationsRequest( + positive_chunk_ids=positive_chunk_ids, + negative_chunk_ids=negative_chunk_ids or [], + limit=limit + ) + + self._log(f"Getting recommendations based on {len(positive_chunk_ids)} positive examples") + response = await self.client.post( + f"{self.base_url}/api/vector/recommendations", + json=request.model_dump() + ) + response.raise_for_status() + return SemanticSearchResponse(**response.json()) + + # ==================== Document Management ==================== + + async def list_documents(self) -> DocumentsResponse: + """ + List all documents in the CZero Engine system. + + Returns: + DocumentsResponse with document metadata + """ + self._log("Fetching document list...") + response = await self.client.get(f"{self.base_url}/api/documents") + response.raise_for_status() + return DocumentsResponse(**response.json()) + + # ==================== Embedding Generation ==================== + + async def generate_embedding( + self, + text: str, + model_id: Optional[str] = None + ) -> EmbeddingResponse: + """ + Generate vector embeddings for text. + + These embeddings can be used for similarity comparisons + or custom vector operations. + + Args: + text: Text to generate embedding for + model_id: Optional specific embedding model to use + + Returns: + EmbeddingResponse with embedding vector + """ + request = EmbeddingRequest( + text=text, + model_id=model_id + ) + + self._log(f"Generating embedding for text ({len(text)} chars)...") + response = await self.client.post( + f"{self.base_url}/api/embeddings/generate", + json=request.model_dump(exclude_none=True) + ) + response.raise_for_status() + return EmbeddingResponse(**response.json()) + + # ==================== Workspace Management ==================== + + async def create_workspace( + self, + name: str, + path: str, + description: Optional[str] = None + ) -> WorkspaceResponse: + """ + Create a new workspace for document organization. + + Workspaces allow you to organize documents and process them + as separate knowledge bases. + + Args: + name: Workspace name + path: Filesystem path for the workspace + description: Optional description + + Returns: + WorkspaceResponse with workspace ID and details + """ + request = WorkspaceCreateRequest( + name=name, + path=path, + description=description + ) + + self._log(f"Creating workspace: {name}") + response = await self.client.post( + f"{self.base_url}/api/workspaces/create", + json=request.model_dump(exclude_none=True) + ) + response.raise_for_status() + return WorkspaceResponse(**response.json()) + + async def process_files( + self, + workspace_id: str, + files: List[str], + chunk_size: int = 1000, + chunk_overlap: int = 200, + embedding_model: Optional[str] = None + ) -> ProcessFilesResponse: + """ + Process files and add them to a workspace. + + This will: + 1. Extract text from documents + 2. Split into chunks + 3. Generate embeddings + 4. Store in vector database + + Args: + workspace_id: ID of the workspace to add files to + files: List of file paths to process + chunk_size: Size of text chunks in characters + chunk_overlap: Overlap between chunks + embedding_model: Optional specific embedding model + + Returns: + ProcessFilesResponse with processing statistics + """ + config = ProcessingConfig( + chunk_size=chunk_size, + chunk_overlap=chunk_overlap, + embedding_model=embedding_model + ) + + request = ProcessFilesRequest( + workspace_id=workspace_id, + files=files, + config=config + ) + + self._log(f"Processing {len(files)} files for workspace {workspace_id}") + response = await self.client.post( + f"{self.base_url}/api/workspaces/process", + json=request.model_dump(exclude_none=True) + ) + response.raise_for_status() + return ProcessFilesResponse(**response.json()) + + # ==================== Persona Endpoints ==================== + + async def list_personas(self) -> PersonaListResponse: + """ + Get list of available AI personas. + + Personas provide specialized interaction styles and expertise. + + Returns: + PersonaListResponse with available personas + """ + self._log("Fetching persona list...") + response = await self.client.get(f"{self.base_url}/api/personas/list") + response.raise_for_status() + return PersonaListResponse(**response.json()) + + async def persona_chat( + self, + persona_id: str, + message: str, + model_id: Optional[str] = None, + system_prompt_template: Optional[str] = None, + conversation_history: Optional[List[Dict[str, str]]] = None, + max_tokens: int = 1024, + temperature: float = 0.7 + ) -> PersonaChatResponse: + """ + Chat with a specific AI persona. + + Each persona has its own personality, expertise, and interaction style. + + Args: + persona_id: ID of the persona to chat with + message: User message + model_id: Optional specific model to use + system_prompt_template: Optional custom system prompt + conversation_history: Optional conversation history for context + max_tokens: Maximum tokens to generate + temperature: Temperature for generation + + Returns: + PersonaChatResponse with persona's response + """ + request = PersonaChatRequest( + persona_id=persona_id, + message=message, + model_id=model_id, + system_prompt_template=system_prompt_template, + conversation_history=conversation_history, + max_tokens=max_tokens, + temperature=temperature + ) + + self._log(f"Chatting with persona: {persona_id}") + response = await self.client.post( + f"{self.base_url}/api/personas/chat", + json=request.model_dump(exclude_none=True) + ) + response.raise_for_status() + return PersonaChatResponse(**response.json()) + + # ==================== Utility Methods ==================== + + def print_search_results(self, response: SemanticSearchResponse): + """ + Pretty print search results to console. + + Args: + response: Search response to display + """ + if not response.results: + console.print("[yellow]No results found[/yellow]") + return + + table = Table(title="Search Results") + table.add_column("Score", style="cyan", width=10) + table.add_column("Doc ID", style="magenta", width=20) + table.add_column("Content", style="green", width=60) + + for result in response.results[:10]: + content_preview = result.content[:100] + "..." if len(result.content) > 100 else result.content + table.add_row( + f"{result.similarity:.3f}", + result.document_id[:20] + "..." if len(result.document_id) > 20 else result.document_id, + content_preview + ) + + console.print(table) + + def print_documents(self, response: DocumentsResponse): + """ + Pretty print document list to console. + + Args: + response: Documents response to display + """ + if not response.documents: + console.print("[yellow]No documents found[/yellow]") + return + + table = Table(title=f"Documents ({len(response.documents)} total)") + table.add_column("Title", style="cyan", width=30) + table.add_column("Type", style="yellow", width=15) + table.add_column("Size", style="magenta", width=10) + table.add_column("Workspace", style="green", width=20) + + for doc in response.documents[:20]: + size_kb = doc.size / 1024 + table.add_row( + doc.title[:30], + doc.content_type, + f"{size_kb:.1f} KB", + doc.workspace_id or "None" + ) + + console.print(table) + + if len(response.documents) > 20: + console.print(f"[dim]... and {len(response.documents) - 20} more[/dim]") + + +# Convenience function for quick usage +async def quick_chat(message: str, use_rag: bool = True) -> str: + """ + Quick helper function to send a chat message. + + Args: + message: Message to send + use_rag: Whether to use RAG + + Returns: + Response text + """ + async with CZeroEngineClient() as client: + response = await client.chat(message, use_rag=use_rag) + return response.response \ No newline at end of file diff --git a/src/czero_engine/models.py b/src/czero_engine/models.py new file mode 100644 index 0000000..1bc37a1 --- /dev/null +++ b/src/czero_engine/models.py @@ -0,0 +1,201 @@ +"""Data models for CZero Engine API - matching actual Rust implementation.""" + +from typing import Any, Dict, List, Optional +from pydantic import BaseModel, Field +from datetime import datetime + + +# Chat/LLM Models +class RagConfig(BaseModel): + """RAG configuration for chat requests.""" + similarity_threshold: float = 0.7 + chunk_limit: int = 5 + + +class ChatRequest(BaseModel): + """Request model for /api/chat/send endpoint.""" + message: str + model_id: Optional[str] = None + use_rag: bool = True + rag_config: Optional[RagConfig] = None + use_web_search: bool = False + system_prompt: Optional[str] = None + max_tokens: Optional[int] = 1024 + temperature: Optional[float] = 0.7 + + +class ContextChunk(BaseModel): + """Context chunk used in chat responses.""" + chunk_id: str + content: str + similarity: float + + +class ChatResponse(BaseModel): + """Response model for /api/chat/send endpoint.""" + response: str + model_used: str + context_used: Optional[List[ContextChunk]] = None + cacheable: bool = False + + +# Vector Search Models +class SemanticSearchRequest(BaseModel): + """Request model for /api/vector/search/semantic endpoint.""" + query: str + limit: int = 10 + similarity_threshold: float = 0.7 + include_content: bool = True + workspace_filter: Optional[str] = None + + +class SearchResult(BaseModel): + """Individual search result.""" + chunk_id: str + document_id: str + content: str + similarity: float + metadata: Dict[str, Any] = Field(default_factory=dict) + + +class SemanticSearchResponse(BaseModel): + """Response model for semantic search endpoints.""" + results: List[SearchResult] + + +class SimilaritySearchRequest(BaseModel): + """Request model for /api/vector/search/similarity endpoint.""" + chunk_id: str + limit: int = 5 + similarity_threshold: float = 0.5 + + +class RecommendationsRequest(BaseModel): + """Request model for /api/vector/recommendations endpoint.""" + positive_chunk_ids: List[str] + negative_chunk_ids: Optional[List[str]] = Field(default_factory=list) + limit: int = 10 + + +# Document Models +class DocumentMetadata(BaseModel): + """Document metadata model.""" + id: str + title: str + path: str + content_type: str + size: int + created_at: str + updated_at: str + workspace_id: Optional[str] = None + + +class DocumentsResponse(BaseModel): + """Response model for /api/documents endpoint.""" + documents: List[DocumentMetadata] + + +# Embedding Models +class EmbeddingRequest(BaseModel): + """Request model for /api/embeddings/generate endpoint.""" + text: str + model_id: Optional[str] = None + + +class EmbeddingResponse(BaseModel): + """Response model for /api/embeddings/generate endpoint.""" + embedding: List[float] + model_used: str + + +# Workspace Models +class WorkspaceCreateRequest(BaseModel): + """Request model for /api/workspaces/create endpoint.""" + name: str + path: str + description: Optional[str] = None + + +class WorkspaceResponse(BaseModel): + """Response model for workspace creation.""" + id: str + name: str + path: str + description: Optional[str] = None + created_at: str + + +class ProcessingConfig(BaseModel): + """Configuration for file processing.""" + chunk_size: Optional[int] = 1000 + chunk_overlap: Optional[int] = 200 + embedding_model: Optional[str] = None + + +class ProcessFilesRequest(BaseModel): + """Request model for /api/workspaces/process endpoint.""" + workspace_id: str + files: List[str] + config: Optional[ProcessingConfig] = None + + +class ProcessFilesResponse(BaseModel): + """Response model for file processing.""" + status: str + workspace_id: str + files_processed: int + files_failed: int + chunks_created: int + processing_time: float + message: str + + +# Persona Models +class PersonaInfo(BaseModel): + """Information about an AI persona.""" + id: str + name: str + tagline: Optional[str] = None + specialty: str + avatar: str + is_default: bool = False + is_custom: bool = False + + +class PersonaListResponse(BaseModel): + """Response model for /api/personas/list endpoint.""" + personas: List[PersonaInfo] + + +class ConversationMessage(BaseModel): + """Single message in conversation history.""" + role: str # "user" or "assistant" + content: str + + +class PersonaChatRequest(BaseModel): + """Request model for /api/personas/chat endpoint.""" + persona_id: str + message: str + model_id: Optional[str] = None + system_prompt_template: Optional[str] = None + conversation_history: Optional[List[ConversationMessage]] = None + max_tokens: Optional[int] = 1024 + temperature: Optional[float] = 0.7 + + +class PersonaChatResponse(BaseModel): + """Response model for persona chat.""" + response: str + persona_id: str + model_used: str + timestamp: str + + +# Health Check Model +class HealthResponse(BaseModel): + """Response model for /api/health endpoint.""" + status: str + service: str + version: str + timestamp: str \ No newline at end of file diff --git a/src/czero_engine/workflows/__init__.py b/src/czero_engine/workflows/__init__.py new file mode 100644 index 0000000..12aeb1b --- /dev/null +++ b/src/czero_engine/workflows/__init__.py @@ -0,0 +1,13 @@ +"""CZero Engine workflow implementations.""" + +from .knowledge_base import KnowledgeBaseWorkflow +from .rag_workflow import RAGWorkflow +from .persona_workflow import PersonaWorkflow +from .document_processing import DocumentProcessingWorkflow + +__all__ = [ + "KnowledgeBaseWorkflow", + "RAGWorkflow", + "PersonaWorkflow", + "DocumentProcessingWorkflow", +] \ No newline at end of file diff --git a/src/czero_engine/workflows/document_processing.py b/src/czero_engine/workflows/document_processing.py new file mode 100644 index 0000000..dfb0864 --- /dev/null +++ b/src/czero_engine/workflows/document_processing.py @@ -0,0 +1,507 @@ +"""Document Processing workflow for CZero Engine - Advanced document handling.""" + +from typing import Optional, List, Dict, Any, Tuple +from pathlib import Path +from dataclasses import dataclass +import mimetypes +import asyncio +from rich.console import Console +from rich.table import Table +from rich.progress import Progress, SpinnerColumn, TextColumn +from rich.tree import Tree + +from ..client import CZeroEngineClient +from ..models import ( + WorkspaceResponse, + ProcessFilesResponse, + DocumentsResponse, + EmbeddingResponse +) + +console = Console() + + +@dataclass +class ProcessingStats: + """Statistics for document processing.""" + total_files: int = 0 + processed_files: int = 0 + failed_files: int = 0 + total_chunks: int = 0 + total_size_bytes: int = 0 + processing_time: float = 0.0 + + @property + def success_rate(self) -> float: + """Calculate success rate.""" + if self.total_files == 0: + return 0.0 + return (self.processed_files / self.total_files) * 100 + + +class DocumentProcessingWorkflow: + """ + Advanced document processing workflow for CZero Engine. + + This workflow provides: + - Intelligent file discovery and filtering + - Batch processing with progress tracking + - Multiple workspace management + - Document deduplication + - Processing statistics and reporting + """ + + def __init__(self, client: Optional[CZeroEngineClient] = None, verbose: bool = True): + """ + Initialize Document Processing workflow. + + Args: + client: Optional CZeroEngineClient instance + verbose: Enable verbose output + """ + self.client = client + self._owns_client = client is None + self.verbose = verbose + self.workspaces: Dict[str, WorkspaceResponse] = {} + self.processing_stats = ProcessingStats() + + async def __aenter__(self): + """Async context manager entry.""" + if self._owns_client: + self.client = CZeroEngineClient(verbose=self.verbose) + await self.client.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + if self._owns_client and self.client: + await self.client.__aexit__(exc_type, exc_val, exc_tb) + + def discover_files( + self, + directory: str, + patterns: Optional[List[str]] = None, + exclude_patterns: Optional[List[str]] = None, + max_size_mb: Optional[float] = None, + min_size_kb: Optional[float] = None + ) -> List[Path]: + """ + Discover files in a directory with advanced filtering. + + Args: + directory: Directory to search + patterns: File patterns to include (e.g., ["*.pdf", "*.txt"]) + exclude_patterns: Patterns to exclude + max_size_mb: Maximum file size in MB + min_size_kb: Minimum file size in KB + + Returns: + List of discovered file paths + """ + path = Path(directory) + if not path.exists(): + console.print(f"[red]Directory not found: {directory}[/red]") + return [] + + # Default patterns for common document types + if patterns is None: + patterns = [ + "*.txt", "*.md", "*.pdf", "*.docx", "*.doc", + "*.py", "*.js", "*.ts", "*.jsx", "*.tsx", + "*.java", "*.cpp", "*.c", "*.h", "*.hpp", + "*.json", "*.yaml", "*.yml", "*.xml", + "*.rst", "*.tex", "*.html", "*.css" + ] + + exclude = exclude_patterns or [ + "*.pyc", "*.pyo", "*.pyd", "*.so", "*.dll", + "*.exe", "*.bin", "*.dat", "*.db", "*.sqlite", + "*.jpg", "*.jpeg", "*.png", "*.gif", "*.mp4", + "*.zip", "*.tar", "*.gz", "*.rar" + ] + + discovered_files = [] + + for pattern in patterns: + for file_path in path.rglob(pattern): + # Skip if matches exclude pattern + if any(file_path.match(exc) for exc in exclude): + continue + + # Skip if not a file + if not file_path.is_file(): + continue + + # Check file size constraints + try: + size_bytes = file_path.stat().st_size + + if max_size_mb and size_bytes > max_size_mb * 1024 * 1024: + continue + + if min_size_kb and size_bytes < min_size_kb * 1024: + continue + + discovered_files.append(file_path) + + except Exception as e: + if self.verbose: + console.print(f"[yellow]Cannot access {file_path}: {e}[/yellow]") + + # Remove duplicates while preserving order + seen = set() + unique_files = [] + for f in discovered_files: + if f not in seen: + seen.add(f) + unique_files.append(f) + + if self.verbose: + console.print(f"[cyan]Discovered {len(unique_files)} files[/cyan]") + + # Show file type distribution + type_counts = {} + for f in unique_files: + ext = f.suffix.lower() + type_counts[ext] = type_counts.get(ext, 0) + 1 + + if type_counts: + table = Table(title="File Types") + table.add_column("Extension", style="cyan") + table.add_column("Count", style="green") + + for ext, count in sorted(type_counts.items(), key=lambda x: x[1], reverse=True)[:10]: + table.add_row(ext or "(no extension)", str(count)) + + console.print(table) + + return unique_files + + async def process_documents( + self, + files: List[Path], + workspace_name: str, + workspace_path: Optional[str] = None, + batch_size: int = 10, + chunk_size: int = 1000, + chunk_overlap: int = 200, + skip_existing: bool = False + ) -> ProcessingStats: + """ + Process a list of documents into a workspace. + + Args: + files: List of file paths to process + workspace_name: Name for the workspace + workspace_path: Optional workspace path (uses first file's directory if not specified) + batch_size: Number of files to process at once + chunk_size: Size of text chunks + chunk_overlap: Overlap between chunks + skip_existing: Skip files that already exist in the workspace + + Returns: + Processing statistics + """ + if not self.client: + raise ValueError("Client not initialized") + + if not files: + console.print("[yellow]No files to process[/yellow]") + return ProcessingStats() + + # Determine workspace path + if not workspace_path: + workspace_path = str(files[0].parent) + + # Create or get workspace + if workspace_name not in self.workspaces: + workspace = await self.client.create_workspace( + name=workspace_name, + path=workspace_path, + description=f"Document workspace with {len(files)} files" + ) + self.workspaces[workspace_name] = workspace + else: + workspace = self.workspaces[workspace_name] + + console.print(f"[green]โœ“[/green] Using workspace: {workspace.id}") + + # Get existing documents if skip_existing is enabled + existing_files = set() + if skip_existing: + docs_response = await self.client.list_documents() + for doc in docs_response.documents: + if doc.workspace_id == workspace.id: + existing_files.add(doc.path) + + if existing_files: + console.print(f"[dim]Skipping {len(existing_files)} existing files[/dim]") + files = [f for f in files if str(f) not in existing_files] + + # Initialize statistics + stats = ProcessingStats(total_files=len(files)) + + # Process files in batches + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + console=console + ) as progress: + + task = progress.add_task( + f"Processing {len(files)} documents...", + total=len(files) + ) + + for i in range(0, len(files), batch_size): + batch = files[i:i+batch_size] + batch_paths = [str(f.absolute()) for f in batch] + + # Calculate batch size + batch_size_bytes = sum(f.stat().st_size for f in batch if f.exists()) + stats.total_size_bytes += batch_size_bytes + + try: + # Process batch + import time + start_time = time.time() + + result = await self.client.process_files( + workspace_id=workspace.id, + files=batch_paths, + chunk_size=chunk_size, + chunk_overlap=chunk_overlap + ) + + processing_time = time.time() - start_time + stats.processing_time += processing_time + + # Update statistics + stats.processed_files += result.files_processed + stats.failed_files += result.files_failed + stats.total_chunks += result.chunks_created + + progress.update(task, advance=len(batch)) + + if self.verbose and result.files_failed > 0: + console.print(f"[yellow]โš  {result.files_failed} files failed in batch[/yellow]") + + except Exception as e: + console.print(f"[red]Batch processing error: {e}[/red]") + stats.failed_files += len(batch) + progress.update(task, advance=len(batch)) + + # Display final statistics + self._display_stats(stats) + + return stats + + async def process_directory_tree( + self, + root_directory: str, + workspace_prefix: str = "workspace", + organize_by_type: bool = True, + **process_kwargs + ) -> Dict[str, ProcessingStats]: + """ + Process an entire directory tree, organizing into multiple workspaces. + + Args: + root_directory: Root directory to process + workspace_prefix: Prefix for workspace names + organize_by_type: Create separate workspaces by file type + **process_kwargs: Additional arguments for process_documents + + Returns: + Dictionary of workspace names to processing statistics + """ + # Discover all files + all_files = self.discover_files(root_directory) + + if not all_files: + console.print("[yellow]No files found to process[/yellow]") + return {} + + workspace_stats = {} + + if organize_by_type: + # Group files by type + file_groups = {} + for file_path in all_files: + mime_type, _ = mimetypes.guess_type(str(file_path)) + if mime_type: + category = mime_type.split('/')[0] + else: + category = file_path.suffix.lower() or "other" + + if category not in file_groups: + file_groups[category] = [] + file_groups[category].append(file_path) + + # Process each group + for category, files in file_groups.items(): + workspace_name = f"{workspace_prefix}_{category}" + + if self.verbose: + console.print(f"\n[bold]Processing {category} files[/bold]") + + stats = await self.process_documents( + files=files, + workspace_name=workspace_name, + workspace_path=root_directory, + **process_kwargs + ) + + workspace_stats[workspace_name] = stats + + else: + # Process all files into single workspace + workspace_name = f"{workspace_prefix}_all" + stats = await self.process_documents( + files=all_files, + workspace_name=workspace_name, + workspace_path=root_directory, + **process_kwargs + ) + workspace_stats[workspace_name] = stats + + # Display summary + self._display_summary(workspace_stats) + + return workspace_stats + + async def generate_embeddings_for_text( + self, + texts: List[str], + model_id: Optional[str] = None + ) -> List[EmbeddingResponse]: + """ + Generate embeddings for a list of texts. + + Args: + texts: List of texts to generate embeddings for + model_id: Optional embedding model to use + + Returns: + List of embedding responses + """ + if not self.client: + raise ValueError("Client not initialized") + + embeddings = [] + + if self.verbose: + console.print(f"[cyan]Generating embeddings for {len(texts)} texts...[/cyan]") + + for text in texts: + try: + embedding = await self.client.generate_embedding( + text=text, + model_id=model_id + ) + embeddings.append(embedding) + except Exception as e: + console.print(f"[red]Failed to generate embedding: {e}[/red]") + + if self.verbose: + console.print(f"[green]โœ“[/green] Generated {len(embeddings)} embeddings") + + return embeddings + + def _display_stats(self, stats: ProcessingStats): + """Display processing statistics.""" + if not self.verbose: + return + + table = Table(title="Processing Statistics") + table.add_column("Metric", style="cyan") + table.add_column("Value", style="green") + + table.add_row("Total Files", str(stats.total_files)) + table.add_row("Processed", str(stats.processed_files)) + table.add_row("Failed", str(stats.failed_files)) + table.add_row("Success Rate", f"{stats.success_rate:.1f}%") + table.add_row("Total Chunks", str(stats.total_chunks)) + table.add_row("Total Size", f"{stats.total_size_bytes / (1024*1024):.2f} MB") + table.add_row("Processing Time", f"{stats.processing_time:.2f} seconds") + + if stats.processing_time > 0: + throughput = stats.total_size_bytes / (1024 * 1024) / stats.processing_time + table.add_row("Throughput", f"{throughput:.2f} MB/s") + + console.print(table) + + def _display_summary(self, workspace_stats: Dict[str, ProcessingStats]): + """Display summary of multiple workspace processing.""" + if not self.verbose or not workspace_stats: + return + + console.print("\n[bold]Processing Summary[/bold]") + + tree = Tree("Workspaces") + + total_files = 0 + total_chunks = 0 + total_time = 0 + + for workspace_name, stats in workspace_stats.items(): + branch = tree.add(f"{workspace_name}") + branch.add(f"Files: {stats.processed_files}/{stats.total_files}") + branch.add(f"Chunks: {stats.total_chunks}") + branch.add(f"Success: {stats.success_rate:.1f}%") + + total_files += stats.processed_files + total_chunks += stats.total_chunks + total_time += stats.processing_time + + console.print(tree) + + console.print(f"\n[bold green]Total:[/bold green]") + console.print(f" Files processed: {total_files}") + console.print(f" Chunks created: {total_chunks}") + console.print(f" Total time: {total_time:.2f} seconds") + + +# Example usage +async def example_document_processing(): + """Example of document processing workflow.""" + + async with DocumentProcessingWorkflow() as workflow: + # Discover files with filtering + files = workflow.discover_files( + directory="./documents", + patterns=["*.pdf", "*.txt", "*.md"], + max_size_mb=10, + min_size_kb=1 + ) + + # Process documents into a workspace + if files: + stats = await workflow.process_documents( + files=files[:20], # Process first 20 files + workspace_name="Technical Docs", + chunk_size=1000, + chunk_overlap=200, + skip_existing=True + ) + + # Process entire directory tree with organization + workspace_stats = await workflow.process_directory_tree( + root_directory="./project", + workspace_prefix="project", + organize_by_type=True, + chunk_size=800, + batch_size=5 + ) + + # Generate embeddings for custom texts + embeddings = await workflow.generate_embeddings_for_text([ + "CZero Engine is a powerful document processing system", + "It provides semantic search and RAG capabilities", + "Documents are processed into vector embeddings" + ]) + + +if __name__ == "__main__": + import asyncio + asyncio.run(example_document_processing()) \ No newline at end of file diff --git a/src/czero_engine/workflows/knowledge_base.py b/src/czero_engine/workflows/knowledge_base.py new file mode 100644 index 0000000..7827e09 --- /dev/null +++ b/src/czero_engine/workflows/knowledge_base.py @@ -0,0 +1,340 @@ +"""Knowledge Base workflow for CZero Engine - Build and query document knowledge bases.""" + +from typing import List, Optional, Dict, Any +from pathlib import Path +import asyncio +from rich.console import Console +from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn +from rich.panel import Panel +from rich.table import Table + +from ..client import CZeroEngineClient +from ..models import WorkspaceResponse, ProcessFilesResponse, SemanticSearchResponse + +console = Console() + + +class KnowledgeBaseWorkflow: + """ + Workflow for creating and querying knowledge bases in CZero Engine. + + This workflow helps you: + 1. Create workspaces for organizing documents + 2. Process documents (PDFs, text files, code, etc.) + 3. Build searchable knowledge bases with vector embeddings + 4. Query your knowledge base with semantic search + """ + + def __init__(self, client: Optional[CZeroEngineClient] = None, verbose: bool = True): + """ + Initialize Knowledge Base workflow. + + Args: + client: Optional CZeroEngineClient instance (creates one if not provided) + verbose: Enable verbose output + """ + self.client = client + self._owns_client = client is None + self.verbose = verbose + self.workspace_id: Optional[str] = None + self.workspace_name: Optional[str] = None + + async def __aenter__(self): + """Async context manager entry.""" + if self._owns_client: + self.client = CZeroEngineClient(verbose=self.verbose) + await self.client.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + if self._owns_client and self.client: + await self.client.__aexit__(exc_type, exc_val, exc_tb) + + async def create_knowledge_base( + self, + name: str, + directory_path: str, + file_patterns: Optional[List[str]] = None, + chunk_size: int = 1000, + chunk_overlap: int = 200, + description: Optional[str] = None + ) -> Dict[str, Any]: + """ + Create a complete knowledge base from a directory of documents. + + Args: + name: Name for the knowledge base/workspace + directory_path: Path to directory containing documents + file_patterns: Optional file patterns to include (e.g., ["*.pdf", "*.txt"]) + chunk_size: Size of text chunks for processing + chunk_overlap: Overlap between chunks + description: Optional description for the workspace + + Returns: + Dictionary with workspace details and processing statistics + """ + if not self.client: + raise ValueError("Client not initialized") + + console.print(Panel( + f"[bold cyan]Creating Knowledge Base: {name}[/bold cyan]", + expand=False + )) + + # Step 1: Create workspace + if self.verbose: + console.print("[cyan]๐Ÿ“ Creating workspace...[/cyan]") + + workspace = await self.client.create_workspace( + name=name, + path=directory_path, + description=description or f"Knowledge base for {name}" + ) + + self.workspace_id = workspace.id + self.workspace_name = name + + console.print(f"[green]โœ“[/green] Workspace created: {workspace.id}") + + # Step 2: Find files to process + path = Path(directory_path) + if not path.exists(): + raise ValueError(f"Directory not found: {directory_path}") + + patterns = file_patterns or ["*.txt", "*.md", "*.pdf", "*.docx", "*.py", "*.js", "*.json", "*.yaml", "*.yml"] + files_to_process = [] + + for pattern in patterns: + files_to_process.extend(path.rglob(pattern)) + + if not files_to_process: + console.print(f"[yellow]โš  No files found matching patterns: {patterns}[/yellow]") + return { + "workspace": workspace.model_dump(), + "files_processed": 0, + "chunks_created": 0 + } + + console.print(f"[cyan]๐Ÿ“„ Found {len(files_to_process)} files to process[/cyan]") + + # Step 3: Process files in batches + batch_size = 10 + total_processed = 0 + total_chunks = 0 + failed_files = 0 + + with Progress( + SpinnerColumn(), + TextColumn("[progress.description]{task.description}"), + BarColumn(), + console=console + ) as progress: + task = progress.add_task( + f"Processing {len(files_to_process)} files...", + total=len(files_to_process) + ) + + for i in range(0, len(files_to_process), batch_size): + batch = files_to_process[i:i+batch_size] + batch_paths = [str(f.absolute()) for f in batch] + + try: + # Process batch + result = await self.client.process_files( + workspace_id=workspace.id, + files=batch_paths, + chunk_size=chunk_size, + chunk_overlap=chunk_overlap + ) + + total_processed += result.files_processed + total_chunks += result.chunks_created + failed_files += result.files_failed + + progress.update(task, advance=len(batch)) + + if self.verbose and result.files_failed > 0: + console.print(f"[yellow]โš  {result.files_failed} files failed in batch[/yellow]") + + except Exception as e: + console.print(f"[red]Error processing batch: {e}[/red]") + failed_files += len(batch) + progress.update(task, advance=len(batch)) + + # Step 4: Display summary + console.print("\n[bold green]Knowledge Base Created Successfully![/bold green]") + + summary = Table(title="Knowledge Base Summary") + summary.add_column("Metric", style="cyan") + summary.add_column("Value", style="green") + + summary.add_row("Workspace ID", workspace.id) + summary.add_row("Workspace Name", name) + summary.add_row("Files Processed", str(total_processed)) + summary.add_row("Files Failed", str(failed_files)) + summary.add_row("Chunks Created", str(total_chunks)) + summary.add_row("Chunk Size", f"{chunk_size} chars") + summary.add_row("Chunk Overlap", f"{chunk_overlap} chars") + + console.print(summary) + + return { + "workspace": workspace.model_dump(), + "files_processed": total_processed, + "files_failed": failed_files, + "chunks_created": total_chunks, + "processing_config": { + "chunk_size": chunk_size, + "chunk_overlap": chunk_overlap + } + } + + async def query( + self, + query: str, + limit: int = 5, + similarity_threshold: float = 0.7, + workspace_id: Optional[str] = None + ) -> SemanticSearchResponse: + """ + Query the knowledge base with semantic search. + + Args: + query: Search query + limit: Maximum number of results + similarity_threshold: Minimum similarity score + workspace_id: Optional workspace to search (uses current if not specified) + + Returns: + Search results + """ + if not self.client: + raise ValueError("Client not initialized") + + workspace_to_search = workspace_id or self.workspace_id + + if self.verbose: + console.print(f"\n[cyan]๐Ÿ” Searching: {query}[/cyan]") + if workspace_to_search: + console.print(f"[dim]Workspace: {workspace_to_search}[/dim]") + + results = await self.client.semantic_search( + query=query, + limit=limit, + similarity_threshold=similarity_threshold, + workspace_filter=workspace_to_search + ) + + if self.verbose: + self.client.print_search_results(results) + + return results + + async def find_related( + self, + chunk_id: str, + limit: int = 5 + ) -> SemanticSearchResponse: + """ + Find content related to a specific chunk. + + Args: + chunk_id: ID of the reference chunk + limit: Maximum number of results + + Returns: + Related content + """ + if not self.client: + raise ValueError("Client not initialized") + + if self.verbose: + console.print(f"\n[cyan]๐Ÿ”— Finding content related to chunk: {chunk_id}[/cyan]") + + results = await self.client.find_similar_chunks( + chunk_id=chunk_id, + limit=limit + ) + + if self.verbose: + self.client.print_search_results(results) + + return results + + async def get_recommendations( + self, + positive_examples: List[str], + negative_examples: Optional[List[str]] = None, + limit: int = 10 + ) -> SemanticSearchResponse: + """ + Get content recommendations based on examples. + + Args: + positive_examples: Chunk IDs of content you like + negative_examples: Chunk IDs of content to avoid + limit: Maximum number of recommendations + + Returns: + Recommended content + """ + if not self.client: + raise ValueError("Client not initialized") + + if self.verbose: + console.print(f"\n[cyan]๐Ÿ’ก Getting recommendations...[/cyan]") + console.print(f"[dim]Based on {len(positive_examples)} positive examples[/dim]") + if negative_examples: + console.print(f"[dim]Avoiding {len(negative_examples)} negative examples[/dim]") + + results = await self.client.get_recommendations( + positive_chunk_ids=positive_examples, + negative_chunk_ids=negative_examples, + limit=limit + ) + + if self.verbose: + console.print("\n[bold]Recommendations:[/bold]") + self.client.print_search_results(results) + + return results + + +# Example usage +async def example_knowledge_base(): + """Example of creating and querying a knowledge base.""" + + async with KnowledgeBaseWorkflow() as workflow: + # Create knowledge base from documents + kb_info = await workflow.create_knowledge_base( + name="Technical Documentation", + directory_path="./docs", + file_patterns=["*.md", "*.txt", "*.pdf"], + chunk_size=1000, + chunk_overlap=200, + description="Technical documentation and guides" + ) + + # Query the knowledge base + search_results = await workflow.query( + "How does vector search work?", + limit=5 + ) + + # Find related content + if search_results.results: + first_chunk_id = search_results.results[0].chunk_id + related = await workflow.find_related(first_chunk_id, limit=3) + + # Get recommendations + if len(search_results.results) >= 2: + positive_ids = [r.chunk_id for r in search_results.results[:2]] + recommendations = await workflow.get_recommendations( + positive_examples=positive_ids, + limit=5 + ) + + +if __name__ == "__main__": + asyncio.run(example_knowledge_base()) \ No newline at end of file diff --git a/src/czero_engine/workflows/persona_workflow.py b/src/czero_engine/workflows/persona_workflow.py new file mode 100644 index 0000000..5d6db17 --- /dev/null +++ b/src/czero_engine/workflows/persona_workflow.py @@ -0,0 +1,437 @@ +"""Persona workflow for CZero Engine - Interact with specialized AI personas.""" + +from typing import Optional, List, Dict, Any +from dataclasses import dataclass, field +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.markdown import Markdown + +from ..client import CZeroEngineClient +from ..models import PersonaListResponse, PersonaChatResponse, ConversationMessage + +console = Console() + + +@dataclass +class ConversationContext: + """Context for maintaining conversation with a persona.""" + persona_id: str + persona_name: str + conversation_history: List[ConversationMessage] = field(default_factory=list) + turn_count: int = 0 + max_history: int = 10 # Keep last N messages for context + + +class PersonaWorkflow: + """ + Workflow for interacting with AI personas in CZero Engine. + + Personas provide specialized interaction styles and expertise: + - Gestalt: Adaptive general assistant + - Sage: Research and analysis expert + - Pioneer: Innovation and creative solutions + + Each persona maintains conversation context for coherent dialogue. + """ + + def __init__(self, client: Optional[CZeroEngineClient] = None, verbose: bool = True): + """ + Initialize Persona workflow. + + Args: + client: Optional CZeroEngineClient instance + verbose: Enable verbose output + """ + self.client = client + self._owns_client = client is None + self.verbose = verbose + self.available_personas: Optional[PersonaListResponse] = None + self.active_persona: Optional[ConversationContext] = None + + async def __aenter__(self): + """Async context manager entry.""" + if self._owns_client: + self.client = CZeroEngineClient(verbose=self.verbose) + await self.client.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + if self._owns_client and self.client: + await self.client.__aexit__(exc_type, exc_val, exc_tb) + + async def list_personas(self, refresh: bool = False) -> PersonaListResponse: + """ + List available AI personas. + + Args: + refresh: Force refresh of persona list + + Returns: + List of available personas + """ + if not self.client: + raise ValueError("Client not initialized") + + if refresh or not self.available_personas: + self.available_personas = await self.client.list_personas() + + if self.verbose: + table = Table(title="Available Personas") + table.add_column("ID", style="cyan") + table.add_column("Name", style="green") + table.add_column("Specialty", style="yellow") + table.add_column("Tagline", style="dim") + + for persona in self.available_personas.personas: + table.add_row( + persona.id, + persona.name, + persona.specialty, + persona.tagline or "" + ) + + console.print(table) + + return self.available_personas + + async def select_persona(self, persona_id: str) -> ConversationContext: + """ + Select a persona for conversation. + + Args: + persona_id: ID of the persona to select + + Returns: + Conversation context for the selected persona + """ + if not self.available_personas: + await self.list_personas() + + # Find persona info + persona_info = None + for persona in self.available_personas.personas: + if persona.id == persona_id: + persona_info = persona + break + + if not persona_info: + raise ValueError(f"Persona not found: {persona_id}") + + # Create conversation context + self.active_persona = ConversationContext( + persona_id=persona_id, + persona_name=persona_info.name + ) + + if self.verbose: + console.print(Panel( + f"[bold cyan]{persona_info.name}[/bold cyan]\n" + f"{persona_info.specialty}\n" + f"[dim]{persona_info.tagline}[/dim]", + title="Active Persona", + expand=False + )) + + return self.active_persona + + async def chat( + self, + message: str, + persona_id: Optional[str] = None, + model_id: Optional[str] = None, + system_prompt_template: Optional[str] = None, + max_tokens: int = 1024, + temperature: float = 0.7, + maintain_history: bool = True + ) -> PersonaChatResponse: + """ + Chat with a persona. + + Args: + message: Message to send + persona_id: Optional persona ID (uses active if not specified) + model_id: Optional specific model to use + system_prompt_template: Optional custom system prompt + max_tokens: Maximum tokens in response + temperature: Generation temperature + maintain_history: Whether to maintain conversation history + + Returns: + Persona's response + """ + if not self.client: + raise ValueError("Client not initialized") + + # Determine which persona to use + if persona_id: + if not self.active_persona or self.active_persona.persona_id != persona_id: + await self.select_persona(persona_id) + elif not self.active_persona: + # Default to Gestalt + await self.select_persona("gestalt-default") + + if not self.active_persona: + raise ValueError("No persona selected") + + # Prepare conversation history + history = None + if maintain_history and self.active_persona.conversation_history: + # Keep only recent history + recent_history = self.active_persona.conversation_history[-self.active_persona.max_history:] + history = [msg.model_dump() for msg in recent_history] + + # Send message + if self.verbose: + console.print(f"\n[cyan]You:[/cyan] {message}") + + response = await self.client.persona_chat( + persona_id=self.active_persona.persona_id, + message=message, + model_id=model_id, + system_prompt_template=system_prompt_template, + conversation_history=history, + max_tokens=max_tokens, + temperature=temperature + ) + + # Update conversation history + if maintain_history: + self.active_persona.conversation_history.append( + ConversationMessage(role="user", content=message) + ) + self.active_persona.conversation_history.append( + ConversationMessage(role="assistant", content=response.response) + ) + self.active_persona.turn_count += 1 + + # Display response + if self.verbose: + console.print(f"[green]{self.active_persona.persona_name}:[/green]") + console.print(Panel( + Markdown(response.response), + expand=False + )) + + return response + + async def multi_persona_discussion( + self, + topic: str, + persona_ids: List[str], + rounds: int = 3 + ) -> List[Dict[str, Any]]: + """ + Have multiple personas discuss a topic. + + Each persona provides their perspective based on their specialty. + + Args: + topic: Topic to discuss + persona_ids: List of persona IDs to participate + rounds: Number of discussion rounds + + Returns: + List of responses from each persona + """ + if not self.client: + raise ValueError("Client not initialized") + + if self.verbose: + console.print(Panel( + f"[bold cyan]Multi-Persona Discussion[/bold cyan]\n" + f"Topic: {topic}\n" + f"Participants: {', '.join(persona_ids)}\n" + f"Rounds: {rounds}", + expand=False + )) + + discussion_log = [] + current_topic = topic + + for round_num in range(rounds): + if self.verbose: + console.print(f"\n[bold]Round {round_num + 1}[/bold]") + + round_responses = [] + + for persona_id in persona_ids: + # Build prompt with previous responses + if round_responses: + previous = "\n".join([ + f"{r['persona']}: {r['response'][:200]}..." + for r in round_responses + ]) + prompt = f"""Topic: {current_topic} + +Previous responses in this round: +{previous} + +Please provide your perspective on this topic.""" + else: + prompt = f"Please provide your perspective on: {current_topic}" + + # Get persona response + response = await self.chat( + message=prompt, + persona_id=persona_id, + maintain_history=False # Don't maintain history for discussions + ) + + round_responses.append({ + "persona": persona_id, + "response": response.response, + "round": round_num + 1 + }) + + discussion_log.extend(round_responses) + + # Generate next round's topic based on responses + if round_num < rounds - 1: + synthesis = " ".join([r["response"][:100] for r in round_responses]) + next_topic_prompt = f"""Based on these perspectives on "{current_topic}": +{synthesis} + +What follow-up question or aspect should be explored next? Provide only the question.""" + + next_response = await self.client.chat( + message=next_topic_prompt, + use_rag=False, + max_tokens=100 + ) + current_topic = next_response.response + + if self.verbose: + console.print(f"\n[dim]Next topic: {current_topic}[/dim]") + + return discussion_log + + async def persona_comparison( + self, + question: str, + persona_ids: Optional[List[str]] = None + ) -> Dict[str, PersonaChatResponse]: + """ + Compare how different personas respond to the same question. + + Args: + question: Question to ask all personas + persona_ids: List of personas to compare (uses all if not specified) + + Returns: + Dictionary of persona responses + """ + if not self.client: + raise ValueError("Client not initialized") + + # Get all personas if not specified + if not persona_ids: + personas = await self.list_personas() + persona_ids = [p.id for p in personas.personas] + + if self.verbose: + console.print(Panel( + f"[bold cyan]Persona Comparison[/bold cyan]\n" + f"Question: {question}", + expand=False + )) + + responses = {} + + for persona_id in persona_ids: + if self.verbose: + console.print(f"\n[cyan]Asking {persona_id}...[/cyan]") + + response = await self.chat( + message=question, + persona_id=persona_id, + maintain_history=False + ) + + responses[persona_id] = response + + return responses + + def get_conversation_summary(self) -> Dict[str, Any]: + """ + Get summary of current conversation. + + Returns: + Conversation statistics and recent history + """ + if not self.active_persona: + return {"error": "No active conversation"} + + summary = { + "persona": self.active_persona.persona_name, + "persona_id": self.active_persona.persona_id, + "turn_count": self.active_persona.turn_count, + "message_count": len(self.active_persona.conversation_history), + "recent_messages": [ + msg.model_dump() + for msg in self.active_persona.conversation_history[-6:] + ] + } + + if self.verbose: + console.print(Panel( + f"[bold]Conversation Summary[/bold]\n" + f"Persona: {summary['persona']}\n" + f"Turns: {summary['turn_count']}\n" + f"Messages: {summary['message_count']}", + expand=False + )) + + return summary + + def reset_conversation(self): + """Reset the current conversation context.""" + if self.active_persona: + self.active_persona.conversation_history = [] + self.active_persona.turn_count = 0 + + if self.verbose: + console.print("[yellow]Conversation reset[/yellow]") + + +# Example usage +async def example_personas(): + """Example of using personas workflow.""" + + async with PersonaWorkflow() as workflow: + # List available personas + await workflow.list_personas() + + # Chat with Gestalt (default persona) + response = await workflow.chat( + "Hello! What makes you unique as an AI assistant?" + ) + + # Continue conversation + response = await workflow.chat( + "Can you help me understand semantic search?" + ) + + # Switch to Sage persona + await workflow.select_persona("sage") + response = await workflow.chat( + "What are the philosophical implications of AI?" + ) + + # Multi-persona discussion + discussion = await workflow.multi_persona_discussion( + topic="The future of human-AI collaboration", + persona_ids=["gestalt-default", "sage", "pioneer"], + rounds=2 + ) + + # Compare persona responses + comparison = await workflow.persona_comparison( + "How should we approach learning new technologies?" + ) + + +if __name__ == "__main__": + import asyncio + asyncio.run(example_personas()) \ No newline at end of file diff --git a/src/czero_engine/workflows/rag_workflow.py b/src/czero_engine/workflows/rag_workflow.py new file mode 100644 index 0000000..48bd740 --- /dev/null +++ b/src/czero_engine/workflows/rag_workflow.py @@ -0,0 +1,357 @@ +"""RAG (Retrieval Augmented Generation) workflow for CZero Engine.""" + +from typing import Optional, List, Dict, Any +from rich.console import Console +from rich.panel import Panel +from rich.markdown import Markdown +from rich.table import Table + +from ..client import CZeroEngineClient +from ..models import ChatResponse, SemanticSearchResponse + +console = Console() + + +class RAGWorkflow: + """ + Workflow for Retrieval Augmented Generation using CZero Engine. + + This workflow combines semantic search with LLM generation to provide + accurate, context-aware responses based on your document knowledge base. + """ + + def __init__(self, client: Optional[CZeroEngineClient] = None, verbose: bool = True): + """ + Initialize RAG workflow. + + Args: + client: Optional CZeroEngineClient instance + verbose: Enable verbose output + """ + self.client = client + self._owns_client = client is None + self.verbose = verbose + self.last_response: Optional[ChatResponse] = None + self.last_search: Optional[SemanticSearchResponse] = None + + async def __aenter__(self): + """Async context manager entry.""" + if self._owns_client: + self.client = CZeroEngineClient(verbose=self.verbose) + await self.client.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Async context manager exit.""" + if self._owns_client and self.client: + await self.client.__aexit__(exc_type, exc_val, exc_tb) + + async def ask( + self, + question: str, + similarity_threshold: float = 0.7, + chunk_limit: int = 5, + max_tokens: int = 1024, + temperature: float = 0.7, + system_prompt: Optional[str] = None, + workspace_filter: Optional[str] = None + ) -> ChatResponse: + """ + Ask a question using RAG - retrieves relevant context then generates response. + + Args: + question: The question to ask + similarity_threshold: Minimum similarity for context chunks + chunk_limit: Maximum number of context chunks to use + max_tokens: Maximum tokens in response + temperature: LLM temperature (0.0-1.0) + system_prompt: Optional system prompt + workspace_filter: Optional workspace to search + + Returns: + Chat response with answer and context used + """ + if not self.client: + raise ValueError("Client not initialized") + + if self.verbose: + console.print(Panel( + f"[bold cyan]RAG Query[/bold cyan]\n{question}", + expand=False + )) + + # Use the chat endpoint with RAG enabled + response = await self.client.chat( + message=question, + use_rag=True, + system_prompt=system_prompt or "You are a helpful assistant. Answer based on the provided context.", + max_tokens=max_tokens, + temperature=temperature, + similarity_threshold=similarity_threshold, + chunk_limit=chunk_limit + ) + + self.last_response = response + + if self.verbose: + # Display response + console.print("\n[bold green]Answer:[/bold green]") + console.print(Panel(Markdown(response.response), expand=False)) + + # Display context used if available + if response.context_used: + console.print(f"\n[dim]Context: {len(response.context_used)} chunks used[/dim]") + + context_table = Table(title="Context Sources") + context_table.add_column("Score", style="cyan", width=10) + context_table.add_column("Content", style="dim", width=80) + + for chunk in response.context_used[:3]: + content_preview = chunk.content[:150] + "..." if len(chunk.content) > 150 else chunk.content + context_table.add_row( + f"{chunk.similarity:.3f}", + content_preview + ) + + console.print(context_table) + + return response + + async def search_then_ask( + self, + search_query: str, + question: Optional[str] = None, + search_limit: int = 10, + use_top_n: int = 5, + similarity_threshold: float = 0.7 + ) -> Dict[str, Any]: + """ + First search for relevant content, then ask a question about it. + + Useful when you want to see the search results before generating an answer. + + Args: + search_query: Query for semantic search + question: Optional different question to ask (uses search_query if not provided) + search_limit: Number of search results to retrieve + use_top_n: Number of top results to use for context + similarity_threshold: Minimum similarity score + + Returns: + Dictionary with search results and generated answer + """ + if not self.client: + raise ValueError("Client not initialized") + + # Step 1: Search + if self.verbose: + console.print(f"\n[cyan]๐Ÿ” Searching: {search_query}[/cyan]") + + search_results = await self.client.semantic_search( + query=search_query, + limit=search_limit, + similarity_threshold=similarity_threshold + ) + + self.last_search = search_results + + if self.verbose: + console.print(f"[green]โœ“[/green] Found {len(search_results.results)} results") + + if not search_results.results: + console.print("[yellow]No relevant content found[/yellow]") + return { + "search_results": search_results.model_dump(), + "answer": None + } + + # Step 2: Build context from top results + context_chunks = [] + for result in search_results.results[:use_top_n]: + context_chunks.append(result.content) + + context = "\n\n".join(context_chunks) + + # Step 3: Ask question with context + final_question = question or search_query + prompt_with_context = f"""Based on the following context, answer this question: {final_question} + +Context: +{context} + +Answer:""" + + if self.verbose: + console.print(f"\n[cyan]๐Ÿ’ฌ Generating answer using top {use_top_n} results...[/cyan]") + + # Use chat without RAG since we already have context + response = await self.client.chat( + message=prompt_with_context, + use_rag=False, # We already have the context + max_tokens=1024 + ) + + if self.verbose: + console.print("\n[bold green]Answer:[/bold green]") + console.print(Panel(Markdown(response.response), expand=False)) + + return { + "search_results": search_results.model_dump(), + "answer": response.model_dump(), + "context_used": use_top_n + } + + async def iterative_refinement( + self, + initial_question: str, + max_iterations: int = 3, + similarity_threshold: float = 0.7 + ) -> List[ChatResponse]: + """ + Iteratively refine answers by asking follow-up questions. + + Args: + initial_question: Starting question + max_iterations: Maximum refinement iterations + similarity_threshold: Minimum similarity for context + + Returns: + List of responses from each iteration + """ + if not self.client: + raise ValueError("Client not initialized") + + responses = [] + current_question = initial_question + + for iteration in range(max_iterations): + if self.verbose: + console.print(f"\n[cyan]Iteration {iteration + 1}/{max_iterations}[/cyan]") + console.print(f"Question: {current_question}") + + # Get response + response = await self.ask( + current_question, + similarity_threshold=similarity_threshold + ) + responses.append(response) + + # Generate follow-up question if not last iteration + if iteration < max_iterations - 1: + follow_up_prompt = f"""Based on this answer: "{response.response}" + +What follow-up question would help clarify or expand on this topic? +Provide only the question, nothing else.""" + + follow_up_response = await self.client.chat( + message=follow_up_prompt, + use_rag=False, + max_tokens=100 + ) + + current_question = follow_up_response.response + + return responses + + async def compare_with_without_rag( + self, + question: str, + **kwargs + ) -> Dict[str, ChatResponse]: + """ + Compare responses with and without RAG for the same question. + + Useful for demonstrating the value of RAG. + + Args: + question: Question to ask + **kwargs: Additional arguments for chat + + Returns: + Dictionary with both responses + """ + if not self.client: + raise ValueError("Client not initialized") + + if self.verbose: + console.print(Panel( + f"[bold cyan]RAG Comparison[/bold cyan]\n{question}", + expand=False + )) + + # Response with RAG + if self.verbose: + console.print("\n[cyan]With RAG (using knowledge base):[/cyan]") + + with_rag = await self.client.chat( + message=question, + use_rag=True, + **kwargs + ) + + if self.verbose: + console.print(Panel( + Markdown(with_rag.response), + title="With RAG", + border_style="green" + )) + if with_rag.context_used: + console.print(f"[dim]Used {len(with_rag.context_used)} context chunks[/dim]") + + # Response without RAG + if self.verbose: + console.print("\n[cyan]Without RAG (LLM only):[/cyan]") + + without_rag = await self.client.chat( + message=question, + use_rag=False, + **kwargs + ) + + if self.verbose: + console.print(Panel( + Markdown(without_rag.response), + title="Without RAG", + border_style="yellow" + )) + + return { + "with_rag": with_rag, + "without_rag": without_rag + } + + +# Example usage +async def example_rag(): + """Example of using RAG workflow.""" + + async with RAGWorkflow() as workflow: + # Simple RAG question + response = await workflow.ask( + "What is CZero Engine and what are its main features?", + chunk_limit=5 + ) + + # Search then ask + result = await workflow.search_then_ask( + search_query="vector embeddings semantic search", + question="How does semantic search work with embeddings?", + use_top_n=3 + ) + + # Compare with and without RAG + comparison = await workflow.compare_with_without_rag( + "Explain the document processing pipeline", + max_tokens=500 + ) + + # Iterative refinement + responses = await workflow.iterative_refinement( + "What is RAG?", + max_iterations=2 + ) + + +if __name__ == "__main__": + import asyncio + asyncio.run(example_rag()) \ No newline at end of file diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..ed0b048 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,225 @@ +"""Integration tests for CZero Engine Python SDK.""" + +import pytest +import asyncio +from pathlib import Path +from czero_engine import CZeroEngineClient +from czero_engine.workflows import ( + KnowledgeBaseWorkflow, + RAGWorkflow, + PersonaWorkflow, + DocumentProcessingWorkflow +) + + +@pytest.mark.asyncio +async def test_client_health_check(): + """Test API health check.""" + async with CZeroEngineClient() as client: + health = await client.health_check() + assert health["status"] == "healthy" + assert "version" in health + + +@pytest.mark.asyncio +async def test_workspace_creation(): + """Test workspace creation.""" + async with CZeroEngineClient() as client: + workspace = await client.create_workspace( + name="Test Workspace", + path="./test_workspace" + ) + assert workspace.id + assert workspace.name == "Test Workspace" + + +@pytest.mark.asyncio +async def test_chat_without_rag(): + """Test chat without RAG.""" + async with CZeroEngineClient() as client: + response = await client.chat( + message="What is 2+2?", + use_rag=False + ) + assert response.response + assert response.model_used + + +@pytest.mark.asyncio +async def test_embedding_generation(): + """Test embedding generation.""" + async with CZeroEngineClient() as client: + embedding = await client.generate_embedding( + text="Test text for embedding" + ) + assert embedding.embedding + assert len(embedding.embedding) > 0 + assert embedding.model_used + + +@pytest.mark.asyncio +async def test_persona_list(): + """Test listing personas.""" + async with CZeroEngineClient() as client: + personas = await client.list_personas() + assert personas.personas + assert len(personas.personas) > 0 + + # Check for expected personas + persona_ids = [p.id for p in personas.personas] + assert "gestalt-default" in persona_ids + + +@pytest.mark.asyncio +async def test_knowledge_base_workflow(): + """Test knowledge base workflow.""" + # Create test documents + test_dir = Path("./test_kb_docs") + test_dir.mkdir(exist_ok=True) + + (test_dir / "doc1.txt").write_text("This is a test document about AI.") + (test_dir / "doc2.txt").write_text("Machine learning is a subset of AI.") + + try: + async with KnowledgeBaseWorkflow() as workflow: + result = await workflow.create_knowledge_base( + name="Test KB", + directory_path=str(test_dir), + chunk_size=100 + ) + + assert result["workspace_id"] + assert result["files_processed"] >= 2 + assert result["chunks_created"] > 0 + + # Test query + results = await workflow.query("What is AI?") + assert results + + finally: + # Cleanup + import shutil + shutil.rmtree(test_dir) + + +@pytest.mark.asyncio +async def test_rag_workflow(): + """Test RAG workflow.""" + async with RAGWorkflow() as workflow: + # Ask without RAG first + response = await workflow.ask( + question="What is CZero Engine?", + chunk_limit=0 # No RAG + ) + assert response.response + assert not response.context_used + + # Note: With RAG would require documents to be indexed first + + +@pytest.mark.asyncio +async def test_persona_workflow(): + """Test persona workflow.""" + async with PersonaWorkflow(verbose=False) as workflow: + # List personas + personas = await workflow.list_personas() + assert personas.personas + + # Select and chat with a persona + context = await workflow.select_persona("gestalt-default") + assert context.persona_id == "gestalt-default" + + response = await workflow.chat( + message="Hello, how are you?", + maintain_history=True + ) + assert response.response + assert response.persona_id == "gestalt-default" + + # Check conversation history + summary = workflow.get_conversation_summary() + assert summary["turn_count"] == 1 + assert summary["message_count"] == 2 # User + assistant + + +@pytest.mark.asyncio +async def test_document_processing_workflow(): + """Test document processing workflow.""" + # Create test files + test_dir = Path("./test_processing") + test_dir.mkdir(exist_ok=True) + + for i in range(3): + (test_dir / f"file_{i}.txt").write_text(f"Test content {i}" * 50) + + try: + async with DocumentProcessingWorkflow(verbose=False) as workflow: + # Discover files + files = workflow.discover_files(str(test_dir)) + assert len(files) == 3 + + # Process documents + stats = await workflow.process_documents( + files=files, + workspace_name="Test Processing", + chunk_size=100 + ) + + assert stats.total_files == 3 + assert stats.processed_files > 0 + assert stats.total_chunks > 0 + + finally: + # Cleanup + import shutil + shutil.rmtree(test_dir) + + +@pytest.mark.asyncio +async def test_semantic_search(): + """Test semantic search functionality.""" + async with CZeroEngineClient() as client: + # First create and process some content + workspace = await client.create_workspace( + name="Search Test", + path="./search_test" + ) + + # Note: Would need actual documents processed first + # This tests the API call structure + try: + results = await client.semantic_search( + query="test query", + limit=5 + ) + # Results might be empty if no documents are indexed + assert hasattr(results, 'results') + except Exception as e: + # Expected if no documents are indexed + pass + + +@pytest.mark.asyncio +async def test_error_handling(): + """Test error handling.""" + async with CZeroEngineClient() as client: + # Test with invalid workspace + with pytest.raises(Exception): + await client.process_files( + workspace_id="invalid-workspace-id", + files=["nonexistent.txt"] + ) + + +def test_sync_operations(): + """Test that sync operations raise appropriate errors.""" + client = CZeroEngineClient() + + # Should not be able to use client without async context + with pytest.raises(RuntimeError): + asyncio.run(client.health_check()) + + +if __name__ == "__main__": + # Run tests + pytest.main([__file__, "-v"]) \ No newline at end of file From c599e7174de3801b6715e0d85c36ad2c510b110b Mon Sep 17 00:00:00 2001 From: arahangua Date: Mon, 11 Aug 2025 13:01:21 +0900 Subject: [PATCH 2/2] checkpoint: functionality checked --- .github/workflows/ci.yml | 45 ++ .gitignore | 2 +- CODE_OF_CONDUCT.md | 59 ++ CONTRIBUTING.md | 202 ++++++ LICENSE | 2 +- README.md | 625 ++++++++++------- examples/01_basic_usage.py | 104 ++- examples/02_rag_system.py | 121 +++- examples/03_persona_interactions.py | 272 +++++--- examples/04_document_processing.py | 638 +++++++++++------- examples/05_langgraph_integration.py | 565 ++++++++++++++++ pyproject.toml | 8 + sample_docs/ai_basics.txt | 7 + sample_docs/czero_engine.md | 14 + sample_docs/semantic_search.txt | 7 + src/czero_engine/client.py | 16 +- src/czero_engine/models.py | 6 + .../workflows/document_processing.py | 25 +- .../workflows/persona_workflow.py | 20 +- tests/test_integration.py | 87 ++- 20 files changed, 2123 insertions(+), 702 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 examples/05_langgraph_integration.py create mode 100644 sample_docs/ai_basics.txt create mode 100644 sample_docs/czero_engine.md create mode 100644 sample_docs/semantic_search.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..936815f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Install dependencies + run: | + uv pip install --system -e . + uv pip install --system pytest pytest-asyncio pytest-cov + + - name: Run linting + run: | + uv pip install --system ruff + ruff check src/ + continue-on-error: true + + - name: Run tests + run: | + pytest tests/ -v + continue-on-error: true # Since we need CZero Engine running + + - name: Check examples syntax + run: | + python -m py_compile examples/*.py \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b2da86..0e4fc81 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,7 @@ logs/ Thumbs.db # Project specific -sample_docs/ +# Note: sample_docs/ is used by example 02_rag_system.py sample_project/ batch_test/ test_*/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e1f74db --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,59 @@ +# Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior: + +* The use of sexualized language or imagery, and sexual attention or advances +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information without explicit permission +* Other conduct which could reasonably be considered inappropriate + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at info@czero.cc. + +All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0e646c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,202 @@ +# Contributing to CZero Engine Python SDK + +Thank you for your interest in contributing to the CZero Engine Python SDK! We welcome contributions from the community. + +## ๐Ÿš€ Getting Started + +1. **Fork the Repository** + ```bash + git clone https://github.com/czero/workflow-template.git + cd workflow-template + ``` + +2. **Set Up Development Environment** + ```bash + # Install UV package manager + pip install uv + + # Install dependencies + uv pip install -e ".[dev]" + ``` + +3. **Verify Setup** + ```bash + # Run tests + uv run pytest + + # Check code style + uv run ruff check . + ``` + +## ๐Ÿ“ Development Workflow + +### 1. Create a Feature Branch +```bash +git checkout -b feature/your-feature-name +``` + +### 2. Make Your Changes +- Write clean, readable code +- Follow existing code patterns +- Add type hints for all functions +- Update documentation as needed + +### 3. Test Your Changes +```bash +# Run all tests +uv run pytest + +# Run specific test +uv run pytest tests/test_integration.py::test_your_feature + +# Check coverage +uv run pytest --cov=czero_engine --cov-report=html +``` + +### 4. Submit Pull Request +- Push your branch to your fork +- Create a pull request with clear description +- Link any related issues + +## ๐ŸŽฏ Guidelines + +### Code Style +- Use Python 3.11+ features +- Follow PEP 8 conventions +- Maximum line length: 100 characters +- Use descriptive variable names + +### Testing +- Write tests for new features +- Maintain or improve code coverage +- Test edge cases and error handling +- Use async/await consistently + +### Documentation +- Update docstrings for new functions +- Add examples for complex features +- Keep README.md current +- Document breaking changes + +## ๐Ÿ—๏ธ Project Structure + +``` +workflow-template/ +โ”œโ”€โ”€ czero_engine/ # Main SDK package +โ”‚ โ”œโ”€โ”€ client.py # API client +โ”‚ โ”œโ”€โ”€ models.py # Pydantic models +โ”‚ โ””โ”€โ”€ workflows/ # High-level workflows +โ”œโ”€โ”€ tests/ # Test suite +โ”œโ”€โ”€ examples/ # Usage examples +โ””โ”€โ”€ docs/ # Additional documentation +``` + +## ๐Ÿงช Testing Requirements + +All contributions must: +- Pass existing tests +- Include tests for new features +- Maintain 80%+ code coverage +- Handle errors gracefully + +## ๐Ÿ“ฆ Submitting Changes + +### Pull Request Checklist +- [ ] Tests pass locally +- [ ] Code follows style guidelines +- [ ] Documentation is updated (see below) +- [ ] Commit messages are clear +- [ ] PR description explains changes + +#### What "Documentation is updated" means: +Update documentation when your changes affect: +- **Docstrings**: Add/update function and class docstrings in your code +- **README.md**: Update if you add new features, change SDK usage, or improve examples +- **Examples**: Update or add example scripts if you introduce new functionality +- **Type hints**: Ensure all new functions have proper type annotations +- **CHANGELOG.md**: Add entry for breaking changes or major features (if file exists) + +Examples: +- Adding a new workflow? โ†’ Update README.md with usage example +- New client method? โ†’ Add docstring with parameters and return type +- Improved error handling? โ†’ Update relevant documentation +- Fixed a common issue? โ†’ Consider adding to troubleshooting section + +Note: The CZero Engine API is closed source and cannot be modified by external contributors. This SDK is a client library that interfaces with the existing API. + +### Commit Message Format +``` +type: brief description + +Longer explanation if needed +Fixes #issue_number +``` + +Types: `feat`, `fix`, `docs`, `test`, `refactor`, `perf`, `chore` + +## ๐Ÿ”ง Development Tips + +### Running CZero Engine Locally +1. Download CZero Engine from [czero.cc](https://czero.cc) +2. Start the application +3. Ensure API server is running on port 1421 +4. Load required models through the UI + +### Debug Mode +```python +# Enable verbose logging +client = CZeroEngineClient(verbose=True) + +# Use environment variables +CZERO_API_URL=http://localhost:1421 +CZERO_VERBOSE=true +``` + +### Common Issues +- **Connection refused**: Ensure CZero Engine is running +- **Model not loaded**: Load models through the app UI +- **Timeout errors**: Increase client timeout for LLM operations + +## ๐Ÿค Code of Conduct + +### Our Standards +- Be respectful and inclusive +- Welcome newcomers +- Accept constructive criticism +- Focus on what's best for the community + +### Unacceptable Behavior +- Harassment or discrimination +- Trolling or insulting comments +- Public or private harassment +- Publishing private information + +## ๐Ÿ“‹ Issue Reporting + +When reporting issues, include: +1. Python version and OS +2. CZero Engine version +3. Steps to reproduce +4. Error messages/logs +5. Expected vs actual behavior + +## ๐ŸŽ–๏ธ Recognition + +Contributors are recognized in: +- GitHub contributors page +- Release notes +- Project documentation + +## ๐Ÿ“ž Getting Help + +- ๐Ÿ’ฌ [Discord Community](https://discord.gg/yjEUkUTEak) +- ๐Ÿ› [Issue Tracker](https://github.com/czero/workflow-template/issues) +- ๐Ÿ“ง [Email](mailto:info@czero.cc) + +## ๐Ÿ“œ License + +By contributing, you agree that your contributions will be licensed under the MIT License. + +--- + +Thank you for contributing to CZero Engine! ๐Ÿš€ \ No newline at end of file diff --git a/LICENSE b/LICENSE index 230e944..127d0cf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 CZero Engine +Copyright (c) 2025 Fiefworks, inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5a4018d..3d7596a 100644 --- a/README.md +++ b/README.md @@ -1,401 +1,508 @@ -# CZero Engine Python SDK +# ๐Ÿš€ CZero Engine Python SDK -Official Python SDK and workflow templates for CZero Engine API - a powerful document processing and RAG (Retrieval Augmented Generation) system. +
-## ๐Ÿš€ Features +[![Python](https://img.shields.io/badge/Python-3.11%2B-blue?style=for-the-badge&logo=python)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)](LICENSE) +[![CZero Engine](https://img.shields.io/badge/CZero%20Engine-1.0%2B-purple?style=for-the-badge)](https://github.com/CZero/czero-engine) +[![LangGraph](https://img.shields.io/badge/LangGraph-0.6.4%2B-orange?style=for-the-badge)](https://langchain-ai.github.io/langgraph/) +[![Documentation](https://img.shields.io/badge/Docs-Available-success?style=for-the-badge)](https://czero.cc/docs) -CZero Engine provides: -- **Document Processing**: Extract, chunk, and embed documents (PDFs, text, code, etc.) -- **Vector Search**: Semantic search across your knowledge base -- **RAG System**: Context-aware LLM responses using your documents -- **AI Personas**: Specialized AI assistants (Gestalt, Sage, Pioneer) -- **Workspace Management**: Organize documents into searchable workspaces +**Official Python SDK for CZero Engine** +*Personal AI Interface: full local AI suite with document processing, semantic search, and RAG system with AI personas* -## ๐Ÿ“ฆ Installation +[Installation](#-installation) โ€ข [Quick Start](#-quick-start) โ€ข [Examples](#-examples) โ€ข [API Docs](#-api-reference) โ€ข [Contributing](CONTRIBUTING.md) -### Prerequisites -- Python 3.11 or higher -- CZero Engine running locally (port 1421) -- UV package manager (optional but recommended) +
-### Install with pip -```bash -pip install czero-engine-python -``` +## โœจ Features + +- **๐Ÿ” Semantic Search**: Vector-based search with hierarchical context support +- **๐Ÿ“„ Document Processing**: Extract, chunk, and embed multiple file formats +- **๐Ÿค– RAG System**: Context-aware AI responses using your documents +- **๐ŸŽญ AI Personas**: Gestalt adaptive assistant + custom personas +- **๐Ÿ“Š Workspace Management**: Organize and process documents efficiently (CZero Engine's main offering) +- **โšก High Performance**: Batch processing, streaming responses, GPU acceleration +- **๐Ÿ”— LangGraph Integration**: Build complex AI agents with CZero Engine as backend +- **โ˜๏ธ Cloud AI Compatible**: Combine with OpenAI, Anthropic, Google AI, and more (langchain compatible) + +## ๐Ÿ“ฆ Installation -### Install from source with UV ```bash -git clone https://github.com/czero/workflow-template.git +# From source (currently the only method) +git clone https://github.com/czero-cc/workflow-template.git cd workflow-template - -# Using UV (recommended) -uv venv -source .venv/bin/activate # On Windows: .venv\Scripts\activate uv pip install -e . -# Or using pip -python -m venv .venv -source .venv/bin/activate +# Or with pip pip install -e . + +# Install with optional dependencies +uv pip install -e ".[langgraph]" # For LangGraph integration ``` +**Requirements**: Python 3.11+ | CZero Engine running on port 1421 + ## ๐ŸŽฏ Quick Start -### 1. Check CZero Engine is Running ```python import asyncio from czero_engine import CZeroEngineClient -async def check_health(): +async def main(): async with CZeroEngineClient() as client: + # Check health health = await client.health_check() - print(f"Status: {health.status}") - print(f"Version: {health.version}") - -asyncio.run(check_health()) -``` - -### 2. Create a Knowledge Base -```python -from czero_engine.workflows import KnowledgeBaseWorkflow - -async def create_kb(): - async with KnowledgeBaseWorkflow() as workflow: - # Process documents into a searchable knowledge base - result = await workflow.create_knowledge_base( - name="My Documentation", - directory_path="./docs", - file_patterns=["*.pdf", "*.md", "*.txt"], - chunk_size=1000, - chunk_overlap=200 - ) - print(f"Processed {result['files_processed']} files") - print(f"Created {result['chunks_created']} chunks") - -asyncio.run(create_kb()) -``` - -### 3. Use RAG for Q&A -```python -from czero_engine.workflows import RAGWorkflow - -async def ask_question(): - async with RAGWorkflow() as workflow: - response = await workflow.ask( - question="What is semantic search and how does it work?", - chunk_limit=5, - similarity_threshold=0.7 + print(f"โœ… API Status: {health.status}") + + # Chat with LLM + response = await client.chat( + message="Explain RAG systems", + use_rag=True # Use document context if available ) print(response.response) -asyncio.run(ask_question()) +asyncio.run(main()) ``` -## ๐Ÿ“š Workflows +## ๐Ÿ“š Core Workflows -### Knowledge Base Workflow -Build and query document knowledge bases: +### 1. Knowledge Base Creation ```python from czero_engine.workflows import KnowledgeBaseWorkflow async with KnowledgeBaseWorkflow() as kb: - # Create knowledge base from documents - await kb.create_knowledge_base( + result = await kb.create_knowledge_base( name="Technical Docs", - directory_path="./documents" - ) - - # Search the knowledge base - results = await kb.query("How does vector search work?") - - # Find similar content - similar = await kb.find_related(chunk_id="chunk_123") - - # Get recommendations - recs = await kb.get_recommendations( - positive_examples=["chunk_1", "chunk_2"] + directory_path="./documents", + chunk_size=1000, + chunk_overlap=200 ) + print(f"Processed {result['files_processed']} chunks") # Hierarchical chunking creates multiple chunks per file ``` -### RAG Workflow -Retrieval Augmented Generation for accurate Q&A: +### 2. RAG-Enhanced Q&A ```python from czero_engine.workflows import RAGWorkflow async with RAGWorkflow() as rag: - # Ask with RAG - response = await rag.ask("Explain document embeddings") - - # Search then ask - result = await rag.search_then_ask( - search_query="vector embeddings", - question="How are they generated?" + # Ask with document context + answer = await rag.ask( + question="What are the key features?", + chunk_limit=5, + similarity_threshold=0.7 ) # Compare with/without RAG comparison = await rag.compare_with_without_rag( - "What is CZero Engine?" + question="Explain semantic search" ) ``` -### Persona Workflow -Interact with specialized AI personas: +### 3. Hierarchical Search + +```python +# Search at different hierarchy levels +results = await client.semantic_search( + query="machine learning concepts", + hierarchy_level="0", # Sections only + include_hierarchy=True # Include parent/child context +) +``` + +### 4. AI Persona Interactions ```python from czero_engine.workflows import PersonaWorkflow async with PersonaWorkflow() as personas: - # List available personas - await personas.list_personas() - - # Chat with Gestalt (adaptive assistant) + # Chat with default Gestalt persona + await personas.select_persona("gestalt-default") # Adaptive Intelligence response = await personas.chat( - "Help me understand RAG systems", - persona_id="gestalt-default" + "Analyze the implications of AGI" ) - # Multi-persona discussion - discussion = await personas.multi_persona_discussion( - topic="Future of AI", - persona_ids=["gestalt-default", "sage", "pioneer"], - rounds=3 + # Or chat directly without selecting + response = await personas.chat( + "What are the key features of CZero Engine?", + persona_id="gestalt-default" ) ``` -### Document Processing Workflow -Advanced document processing capabilities: +### 5. LangGraph Integration (NEW!) ```python -from czero_engine.workflows import DocumentProcessingWorkflow - -async with DocumentProcessingWorkflow() as processor: - # Discover files with filtering - files = processor.discover_files( - directory="./docs", - patterns=["*.pdf", "*.md"], - max_size_mb=10 - ) +from langgraph.graph import StateGraph, MessagesState +from langchain_core.messages import AIMessage, HumanMessage +from langchain_core.outputs import ChatResult, ChatGeneration +from langchain_core.language_models import BaseChatModel +from czero_engine import CZeroEngineClient + +# Create a ChatModel wrapper for CZero Engine (simplified from example 05) +class CZeroLLM(BaseChatModel): + client: Optional[CZeroEngineClient] = None + use_rag: bool = True + base_url: str = "http://localhost:1421" - # Process documents - stats = await processor.process_documents( - files=files, - workspace_name="Research", - chunk_size=800 - ) + def __init__(self, **kwargs): + super().__init__(**kwargs) + if not self.client: + self.client = CZeroEngineClient(base_url=self.base_url) - # Process entire directory tree - await processor.process_directory_tree( - root_directory="./project", - organize_by_type=True - ) + async def _agenerate(self, messages, **kwargs): + # Convert messages to prompt for CZero Engine + prompt = messages[-1].content if messages else "" + + # Use CZero Engine for generation with RAG + response = await self.client.chat( + message=prompt, + use_rag=self.use_rag, + max_tokens=1024 + ) + return ChatResult(generations=[ChatGeneration( + message=AIMessage(content=response.response) + )]) + + @property + def _llm_type(self): + return "czero-engine" + +# Use CZero Engine as LLM backend for LangGraph agents +async with CZeroLLM(use_rag=True) as llm: + +# Build complex agent workflows with Command-based routing +workflow = StateGraph(MessagesState) +workflow.add_node("search", search_node) +workflow.add_node("analyze", analyze_node) +graph = workflow.compile() + +# Combine with cloud AI providers +from langchain_openai import ChatOpenAI +from langchain_anthropic import ChatAnthropic + +# Use multiple LLMs in your workflow +cloud_llm = ChatOpenAI(model="gpt-4") # Or Anthropic, Google, etc. +local_llm = CZeroEngineLLM() # Your local CZero Engine + +# The possibilities are endless! ๐Ÿš€ ``` -## ๐Ÿ”ง API Client +## ๐Ÿ”ง Direct API Client -Low-level client for direct API access: +For fine-grained control: ```python -from czero_engine import CZeroEngineClient - -async with CZeroEngineClient() as client: - # Chat with optional RAG - response = await client.chat( - message="What is CZero Engine?", - use_rag=True, - chunk_limit=5 +async with CZeroEngineClient( + base_url="http://localhost:1421", + timeout=60.0 +) as client: + # Create workspace + workspace = await client.create_workspace( + name="Research Papers", + path="./papers" + ) + + # Process documents (uses SmallToBig hierarchical chunking by default) + result = await client.process_files( + workspace_id=workspace.id, + files=["paper1.pdf", "paper2.md"], + chunk_size=500, + chunk_overlap=100 ) # Semantic search results = await client.semantic_search( - query="document processing", + query="neural networks", limit=10, - similarity_threshold=0.7 + include_hierarchy=True ) # Generate embeddings embedding = await client.generate_embedding( - text="Sample text to embed" - ) - - # Create workspace - workspace = await client.create_workspace( - name="My Workspace", - path="./workspace" - ) - - # Process files - result = await client.process_files( - workspace_id=workspace.id, - files=["doc1.pdf", "doc2.txt"], - chunk_size=1000 + text="Advanced AI concepts" ) ``` -## ๐Ÿ“‹ CLI Usage - -The SDK includes a CLI for common operations: +## ๐Ÿ“‹ CLI Interface ```bash -# Check API health -czero health +# Check system health +uv run czero health # Create knowledge base -czero create-kb ./documents --name "My KB" --chunk-size 1000 +uv run czero create-kb ./docs --name "My KB" --chunk-size 1000 -# Search -czero search "query text" --limit 10 +# Search documents +uv run czero search "query text" --limit 10 --threshold 0.7 # Ask with RAG -czero ask "Your question here" --use-rag +uv run czero ask "Your question" --rag --chunks 5 # Chat with persona -czero chat --persona gestalt-default +uv run czero chat --persona gestalt-default + +# List available personas +uv run czero personas + +# List documents +uv run czero documents -# Process documents -czero process ./docs --workspace "Research" +# Process documents in directory +uv run czero process ./docs --workspace "My Docs" --batch-size 10 + +# Generate embeddings +uv run czero embed "some text" --output embedding.json + +# Show version +uv run czero version ``` -## ๐Ÿ—๏ธ Architecture +## ๐Ÿ—๏ธ API Reference + +### Core Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/api/health` | GET | System health check | +| `/api/chat/send` | POST | LLM chat with optional RAG | +| `/api/vector/search/semantic` | POST | Semantic search with hierarchy | +| `/api/vector/search/similarity` | POST | Find similar chunks | +| `/api/embeddings/generate` | POST | Generate text embeddings | +| `/api/workspaces/create` | POST | Create workspace | +| `/api/workspaces/process` | POST | Process documents | +| `/api/personas/list` | GET | List AI personas | +| `/api/personas/chat` | POST | Chat with persona | +| `/api/documents` | GET | List all documents | -### API Endpoints Used +### ๐Ÿ“Š Similarity Scoring with E5 Models -The SDK uses these CZero Engine API endpoints: +CZero Engine uses **E5 embedding models** which are known for their high-quality semantic representations. However, E5 models typically produce similarity scores that cluster above 70%, even for moderately related content. + +**Automatic Score Rescaling**: To provide more intuitive similarity scores, CZero Engine automatically rescales E5 similarity scores: +- Raw E5 scores of 70-100% are rescaled to 0-100% +- This provides better differentiation between content relevance +- Scores below 70% similarity are generally filtered out as irrelevant + +Example rescaling: +- Raw E5: 70% โ†’ Rescaled: 0% (minimum threshold) +- Raw E5: 85% โ†’ Rescaled: 50% (moderate similarity) +- Raw E5: 100% โ†’ Rescaled: 100% (exact match) + +When using the API: +```python +# The similarity scores returned are already rescaled +results = await client.semantic_search( + query="your search query", + similarity_threshold=0.5 # This is post-rescaling (85% raw E5) +) +``` -- **`POST /api/chat/send`** - LLM text generation with optional RAG -- **`POST /api/vector/search/semantic`** - Semantic search across documents -- **`POST /api/vector/search/similarity`** - Find similar chunks -- **`POST /api/vector/recommendations`** - Get content recommendations -- **`GET /api/documents`** - List all documents -- **`POST /api/embeddings/generate`** - Generate text embeddings -- **`POST /api/workspaces/create`** - Create document workspace -- **`POST /api/workspaces/process`** - Process files into workspace -- **`GET /api/personas/list`** - List available AI personas -- **`POST /api/personas/chat`** - Chat with specific persona -- **`GET /api/health`** - Health check +### Request/Response Models -### How It Works +All models are fully typed with Pydantic: -1. **Document Processing**: Documents are extracted, chunked, and converted to vector embeddings -2. **Vector Storage**: Embeddings are stored in a vector database for fast similarity search -3. **RAG Pipeline**: - - User query โ†’ Generate embedding - - Search for similar chunks โ†’ Retrieve context - - Augment prompt with context โ†’ Generate response -4. **Personas**: Specialized system prompts and conversation management +```python +from czero_engine.models import ( + ChatRequest, ChatResponse, + SemanticSearchRequest, SearchResult, + WorkspaceCreate, ProcessingResult, + PersonaChat, PersonaResponse +) +``` -## ๐Ÿ”ฌ Advanced Examples +## ๐Ÿ“– Examples ### Building a Q&A System + ```python async def build_qa_system(docs_dir: str): - # Step 1: Create knowledge base + # 1. Create knowledge base async with KnowledgeBaseWorkflow() as kb: - await kb.create_knowledge_base( - name="QA Knowledge", - directory_path=docs_dir - ) + await kb.create_knowledge_base("QA KB", docs_dir) workspace_id = kb.workspace_id - # Step 2: Set up RAG for Q&A + # 2. Interactive Q&A async with RAGWorkflow() as rag: while True: - question = input("Ask a question (or 'quit'): ") - if question.lower() == 'quit': - break - - response = await rag.ask( - question=question, - workspace_filter=workspace_id - ) - print(f"\nAnswer: {response.response}\n") + q = input("Question: ") + if q == 'quit': break + + answer = await rag.ask(q, workspace_filter=workspace_id) + print(f"Answer: {answer.response}\n") ``` -### Document Comparison +### Document Similarity Analysis + ```python -async def compare_documents(doc1: str, doc2: str): +async def analyze_similarity(doc1: str, doc2: str): async with CZeroEngineClient() as client: # Generate embeddings emb1 = await client.generate_embedding(doc1) emb2 = await client.generate_embedding(doc2) - # Calculate similarity (cosine similarity) + # Calculate cosine similarity import numpy as np - vec1 = np.array(emb1.embedding) - vec2 = np.array(emb2.embedding) + v1, v2 = np.array(emb1.embedding), np.array(emb2.embedding) + similarity = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) - similarity = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) - print(f"Document similarity: {similarity:.3f}") + print(f"Similarity: {similarity:.3f}") ``` ### Batch Processing with Progress + +```python +async with DocumentProcessingWorkflow(verbose=True) as processor: + files = processor.discover_files("./docs", patterns=["*.pdf"]) + + stats = await processor.process_documents( + files=files, + workspace_name="Batch Process", + batch_size=10, # Process 10 files at a time + chunk_size=800 + ) + + print(f"Files submitted: {stats.total_files}") + print(f"Chunks created: {stats.total_chunks}") # Hierarchical chunks + print(f"Est. Success rate: {stats.success_rate:.1f}%") + print(f"Throughput: {stats.total_chunks/stats.processing_time:.1f} chunks/s") +``` + +## ๐ŸŒ Cloud AI Integration + +While CZero Engine is fully self-contained with local LLMs and RAG, you can seamlessly integrate cloud AI providers through LangChain when needed: + ```python -from pathlib import Path - -async def batch_process_with_progress(root_dir: str): - async with DocumentProcessingWorkflow(verbose=True) as processor: - # Discover all documents - files = processor.discover_files( - directory=root_dir, - patterns=["*.pdf", "*.docx", "*.txt"] +from langgraph.graph import StateGraph, MessagesState +from langchain_core.messages import AIMessage +from langchain_openai import ChatOpenAI +from langchain_anthropic import ChatAnthropic +from czero_engine import CZeroEngineClient + +# Build a hybrid workflow: Local RAG + Cloud LLMs +workflow = StateGraph(MessagesState) + +# Use CZero for local RAG and embeddings +async def search_local(state): + async with CZeroEngineClient() as client: + # CZero handles document search locally + results = await client.semantic_search(state["query"]) + return {"context": results} + +# Use cloud LLM for specific tasks if needed +async def generate_cloud(state): + cloud_llm = ChatOpenAI(model="gpt-4") # or Anthropic, Google, etc. + response = await cloud_llm.ainvoke(state["messages"]) + return {"messages": [response]} + +# Or use CZero Engine for everything +async def generate_local(state): + async with CZeroEngineClient() as client: + response = await client.chat( + message=state["messages"][-1].content, + use_rag=True ) - - print(f"Found {len(files)} files to process") - - # Process in batches of 20 - batch_size = 20 - for i in range(0, len(files), batch_size): - batch = files[i:i+batch_size] - print(f"\nProcessing batch {i//batch_size + 1}...") - - stats = await processor.process_documents( - files=batch, - workspace_name=f"Batch_{i//batch_size + 1}", - chunk_size=1000 - ) - - print(f"Success rate: {stats.success_rate:.1f}%") + return {"messages": [AIMessage(content=response.response)]} + +# Mix and match as needed - the choice is yours! +workflow.add_node("search", search_local) +workflow.add_node("generate", generate_local) # or generate_cloud +``` + +The beauty of LangChain compatibility means you can start fully local and add cloud services only when needed! + +## ๐Ÿงช Testing + +```bash +# Run all tests +uv run pytest + +# Run specific test +uv run pytest tests/test_integration.py::test_hierarchical_search + +# Run with coverage +uv run pytest --cov=czero_engine ``` -## ๐Ÿ” Environment Configuration +## ๐Ÿ” Configuration -Create a `.env` file: +Environment variables (`.env` file): ```env -# CZero Engine API Configuration CZERO_API_URL=http://localhost:1421 - -# Optional settings -CZERO_API_TIMEOUT=30.0 -CZERO_VERBOSE=true +CZERO_API_TIMEOUT=60.0 +CZERO_VERBOSE=false ``` +## ๐Ÿ“Š Performance Tips + +1. **Batch Processing**: Process multiple files in parallel +2. **Chunk Size**: 500-1000 tokens for general documents +3. **Hierarchy**: Use hierarchical search for structured documents +4. **Models**: Ensure LLM and embedding models are pre-loaded + ## ๐Ÿค Contributing -Contributions are welcome! Please: -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Add tests if applicable -5. Submit a pull request +We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. -## ๐Ÿ“œ License +## ๐Ÿšข Version Control & Branch Management + +### For Contributors: + +**Branch Strategy:** +- `main` - Stable releases only +- `develop` - Active development branch +- `feature/*` - New features +- `fix/*` - Bug fixes +- `docs/*` - Documentation updates + +**Workflow:** +1. Fork the repository +2. Create feature branch from `develop` +3. Make changes and test +4. Submit PR to `develop` branch +5. After review, we'll merge to `develop` +6. Periodically, `develop` โ†’ `main` for releases + +**Versioning:** +- We follow [Semantic Versioning](https://semver.org/) +- Format: `MAJOR.MINOR.PATCH` +- Example: `1.2.3` + +### Release Process: +```bash +# Tag a release +git tag -a v1.0.0 -m "Release version 1.0.0" +git push origin v1.0.0 -MIT License - see LICENSE file for details +# Create release on GitHub with changelog +``` -## ๐Ÿ“ง Support +## ๐Ÿ“œ License -- Email: info@czero.cc -- Documentation: https://docs.czero.cc -- Issues: https://github.com/czero/workflow-template/issues +MIT License - see [LICENSE](LICENSE) file -## ๐Ÿ”— Related +## ๐Ÿค Support -- [CZero Engine](https://github.com/czero/czero-engine) - Main engine repository -- [CZero Overlay](https://github.com/czero/czero-overlay) - Desktop application -- [API Documentation](https://api.czero.cc/docs) - Full API reference +- ๐Ÿ“ง Email: info@czero.cc +- ๐Ÿ’ฌ Discord: [Join our community](https://discord.gg/yjEUkUTEak) +- ๐Ÿ› Issues: [GitHub Issues](https://github.com/czero-cc/workflow-template/issues) +- ๐Ÿ“š Docs: [Documentation](https://docs.czero.cc) --- -Built with โค๏ธ by the CZero Team \ No newline at end of file +
+

๐ŸŒŸ If you find this useful, please give us a star!

+ +
+ + Made with โค๏ธ by the CZero Team + +

+ + + + View on GitHub + +
\ No newline at end of file diff --git a/examples/01_basic_usage.py b/examples/01_basic_usage.py index e49bf77..19a294b 100644 --- a/examples/01_basic_usage.py +++ b/examples/01_basic_usage.py @@ -1,4 +1,12 @@ -"""Basic usage examples for CZero Engine Python SDK.""" +"""Basic Usage Example - CZero Engine Python SDK + +This example demonstrates fundamental API operations: +- Health checks and status monitoring +- Chat interactions with LLM +- Embedding generation +- Workspace management +- Persona discovery +""" import asyncio from czero_engine import CZeroEngineClient @@ -7,49 +15,77 @@ async def basic_examples(): """Demonstrate basic SDK usage.""" - # Initialize client - async with CZeroEngineClient() as client: + # Initialize client with default settings (localhost:1421) + async with CZeroEngineClient(verbose=True) as client: - # 1. Health check - print("1. Checking API health...") + # 1. Health check - verify API and models are ready + print("\n1. Health Check") + print("=" * 40) health = await client.health_check() - print(f" Status: {health['status']}") - print() + print(f"โœ… Status: {health.status}") + print(f" Version: {health.version}") + print(f" Service: {health.service}") - # 2. Simple chat without RAG - print("2. Chat without RAG...") + # 2. Simple chat without RAG - direct LLM interaction + print("\n2. Chat (No RAG)") + print("=" * 40) response = await client.chat( - message="What is machine learning?", - use_rag=False - ) - print(f" Response: {response.response[:200]}...") - print() - - # 3. Create a workspace - print("3. Creating workspace...") - workspace = await client.create_workspace( - name="Example Workspace", - path="./documents", - description="Test workspace for examples" + message="Explain quantum computing in one sentence.", + use_rag=False, + max_tokens=100 ) - print(f" Workspace ID: {workspace.id}") - print() + print(f"Response: {response.response}") + print(f"Model: {response.model_used}") - # 4. Generate embedding - print("4. Generating embedding...") + # 3. Generate embeddings for semantic similarity + print("\n3. Embedding Generation") + print("=" * 40) embedding = await client.generate_embedding( - text="CZero Engine is a powerful document processing system" + text="Advanced AI document processing with semantic search" ) - print(f" Embedding dimensions: {len(embedding.embedding)}") - print(f" First 5 values: {embedding.embedding[:5]}") - print() + print(f"โœ… Generated {len(embedding.embedding)}-dimensional embedding") + print(f" Model: {embedding.model_used}") + print(f" Sample values: [{embedding.embedding[0]:.4f}, {embedding.embedding[1]:.4f}, ...]") + + # 4. Create a workspace for document organization + print("\n4. Workspace Creation") + print("=" * 40) + import tempfile + import os - # 5. List personas - print("5. Available personas...") + with tempfile.TemporaryDirectory() as temp_dir: + workspace = await client.create_workspace( + name="Demo Workspace", + path=temp_dir, + description="Example workspace for SDK demonstration" + ) + print(f"โœ… Created workspace: {workspace.name}") + print(f" ID: {workspace.id}") + print(f" Path: {workspace.path}") + + # 5. Discover available AI personas + print("\n5. AI Personas") + print("=" * 40) personas = await client.list_personas() - for persona in personas.personas: - print(f" - {persona.name}: {persona.specialty}") + print(f"Found {len(personas.personas)} personas:") + for persona in personas.personas[:5]: # Show first 5 + print(f" โ€ข {persona.name}: {persona.specialty}") + if persona.tagline: + print(f" {persona.tagline[:60]}...") + + +async def main(): + """Run basic examples with error handling.""" + try: + await basic_examples() + print("\nโœ… All basic examples completed successfully!") + except Exception as e: + print(f"\nโŒ Error: {e}") + print("\nMake sure:") + print("1. CZero Engine is running") + print("2. API server is started (port 1421)") + print("3. Models are loaded in the app") if __name__ == "__main__": - asyncio.run(basic_examples()) \ No newline at end of file + asyncio.run(main()) \ No newline at end of file diff --git a/examples/02_rag_system.py b/examples/02_rag_system.py index 7a99cd5..b359bc4 100644 --- a/examples/02_rag_system.py +++ b/examples/02_rag_system.py @@ -1,15 +1,28 @@ -"""RAG (Retrieval Augmented Generation) system example.""" +"""RAG (Retrieval-Augmented Generation) Example - CZero Engine + +This example demonstrates: +- Document processing and chunking +- Semantic search with hierarchical support +- Chat with RAG context +- Similarity-based recommendations +- Comparing responses with and without RAG +""" import asyncio from pathlib import Path +from czero_engine import CZeroEngineClient from czero_engine.workflows import KnowledgeBaseWorkflow, RAGWorkflow async def rag_example(): - """Build and use a RAG system.""" + """Build and use a RAG system with enhanced features.""" + + print("\n๐Ÿš€ RAG System Example") + print("=" * 50) # Step 1: Create knowledge base from documents - print("Step 1: Creating knowledge base...") + print("\n1. Creating Knowledge Base") + print("-" * 30) async with KnowledgeBaseWorkflow() as kb_workflow: # Ensure we have a documents directory @@ -57,13 +70,33 @@ async def rag_example(): chunk_overlap=50 ) - print(f" Created workspace: {result['workspace_id']}") + print(f"โœ… Created workspace: {result['workspace']['name']}") + print(f" ID: {result['workspace']['id']}") print(f" Processed {result['files_processed']} files") print(f" Created {result['chunks_created']} chunks") - print() - # Step 2: Use RAG for Q&A - print("Step 2: Using RAG for questions...") + # Step 2: Demonstrate hierarchical search + print("\n2. Hierarchical Semantic Search") + print("-" * 30) + async with CZeroEngineClient() as client: + # Search with hierarchy support + results = await client.semantic_search( + query="How does AI and machine learning work?", + limit=3, + include_hierarchy=True, + hierarchy_level=None # Search all levels + ) + + print(f"Found {len(results.results)} results with hierarchy:") + for i, res in enumerate(results.results, 1): + print(f"\n {i}. Score: {res.similarity:.3f}") + print(f" {res.content[:100]}...") + if res.parent_chunk: + print(f" โ†ณ Has parent context") + + # Step 3: Use RAG for Q&A + print("\n3. RAG-Enhanced Q&A") + print("-" * 30) async with RAGWorkflow() as rag_workflow: # Ask questions with RAG @@ -75,34 +108,74 @@ async def rag_example(): ] for i, question in enumerate(questions, 1): - print(f"\nQ{i}: {question}") + print(f"\n๐Ÿ“ Q{i}: {question}") response = await rag_workflow.ask( question=question, chunk_limit=3, - similarity_threshold=0.6 + similarity_threshold=0.5 ) - print(f"A{i}: {response.response[:300]}...") + print(f"๐Ÿ’ก A{i}: {response.response[:250]}...") if response.context_used: - print(f" (Used {len(response.context_used)} context chunks)") - - print("\n" + "="*50) - - # Compare with and without RAG - print("\nStep 3: Comparing with/without RAG...") - comparison_q = "What document processing features does CZero Engine provide?" + print(f" ๐Ÿ“š Used {len(response.context_used)} context chunks") + for j, ctx in enumerate(response.context_used[:2], 1): + print(f" {j}. {ctx.content[:60]}...") + # Step 4: Compare with and without RAG + print("\n4. RAG vs Non-RAG Comparison") + print("-" * 30) + comparison_q = "What document processing features does CZero Engine provide?" + + async with RAGWorkflow() as rag_workflow: comparison = await rag_workflow.compare_with_without_rag( question=comparison_q ) - print(f"\nQuestion: {comparison_q}") - print("\nWithout RAG:") - print(f" {comparison['without_rag'][:200]}...") - print("\nWith RAG:") - print(f" {comparison['with_rag'][:200]}...") - print(f"\n Context chunks used: {comparison['chunks_used']}") + print(f"\n๐Ÿค” Question: {comparison_q}") + print("\nโŒ Without RAG (generic response):") + print(f" {comparison['without_rag'][:200]}...") + print("\nโœ… With RAG (context-aware):") + print(f" {comparison['with_rag'][:200]}...") + print(f"\n๐Ÿ“Š Statistics:") + print(f" Context chunks used: {comparison['chunks_used']}") + print(f" Improvement: More specific and accurate with RAG") + + # Step 5: Find similar content + print("\n5. Similarity Search") + print("-" * 30) + async with CZeroEngineClient() as client: + # Get all chunks first + search_res = await client.semantic_search( + query="semantic search", + limit=1 + ) + + if search_res.results: + chunk_id = search_res.results[0].chunk_id + similar = await client.similarity_search( + chunk_id=chunk_id, + limit=3 + ) + + print(f"Content similar to chunk '{chunk_id[:20]}...':\n") + for i, res in enumerate(similar.results, 1): + print(f" {i}. Score: {res.similarity:.3f}") + print(f" {res.content[:80]}...") + + +async def main(): + """Run RAG examples with error handling.""" + try: + await rag_example() + print("\nโœ… RAG examples completed successfully!") + except Exception as e: + print(f"\nโŒ Error: {e}") + print("\nTroubleshooting:") + print("1. Ensure CZero Engine is running") + print("2. Check that API server is active") + print("3. Verify embedding models are loaded") + print("4. Confirm vector database is initialized") if __name__ == "__main__": - asyncio.run(rag_example()) \ No newline at end of file + asyncio.run(main()) \ No newline at end of file diff --git a/examples/03_persona_interactions.py b/examples/03_persona_interactions.py index 66abc9d..455d466 100644 --- a/examples/03_persona_interactions.py +++ b/examples/03_persona_interactions.py @@ -1,144 +1,196 @@ -"""Examples of interacting with AI personas.""" +"""AI Persona Interactions Example - CZero Engine + +This example demonstrates: +- Listing and discovering AI personas +- Chatting with specific personas +- Multi-persona discussions +- Persona comparison on same topics +- Maintaining conversation history +""" import asyncio +from czero_engine import CZeroEngineClient from czero_engine.workflows import PersonaWorkflow async def persona_examples(): - """Demonstrate persona interactions.""" + """Demonstrate persona interactions and capabilities.""" - async with PersonaWorkflow() as workflow: - - # 1. List available personas - print("1. Available Personas:") - print("="*50) - await workflow.list_personas() - print() - - # 2. Chat with different personas - print("2. Individual Persona Chats:") - print("="*50) - - # Chat with Gestalt - print("\n--- Gestalt (Adaptive Assistant) ---") + print("\n๐Ÿค– AI Persona Examples") + print("=" * 50) + + # 1. Discover available personas using direct API + print("\n1. Available Personas") + print("-" * 30) + + async with CZeroEngineClient() as client: + personas = await client.list_personas() + print(f"Found {len(personas.personas)} personas:\n") + + for persona in personas.personas[:5]: # Show first 5 + print(f" ๐Ÿ“Œ {persona.name}") + print(f" Specialty: {persona.specialty}") + if persona.tagline: + print(f" Tagline: {persona.tagline[:80]}...") + print() + + # 2. Chat with Gestalt persona + print("\n2. Gestalt Persona Chat") + print("-" * 30) + + async with PersonaWorkflow(verbose=False) as workflow: + # Chat with Gestalt (Adaptive Intelligence) + print("\n๐Ÿ’ฌ Gestalt - Adaptive Intelligence") await workflow.select_persona("gestalt-default") - response = await workflow.chat( - "Hello! Can you introduce yourself and explain what makes you unique?" - ) - - # Continue conversation - await workflow.chat( - "How would you help someone learn about AI and machine learning?" - ) - - # Chat with Sage - print("\n--- Sage (Research & Analysis) ---") - await workflow.select_persona("sage") - - await workflow.chat( - "What are the philosophical implications of AGI (Artificial General Intelligence)?" - ) - - # Chat with Pioneer - print("\n--- Pioneer (Innovation) ---") - await workflow.select_persona("pioneer") - - await workflow.chat( - "What innovative applications could combine AR/VR with AI?" - ) - - # 3. Multi-persona discussion - print("\n3. Multi-Persona Discussion:") - print("="*50) - - discussion = await workflow.multi_persona_discussion( - topic="The role of AI in education: opportunities and challenges", - persona_ids=["gestalt-default", "sage", "pioneer"], - rounds=2 - ) - - print("\nDiscussion Summary:") - for entry in discussion: - print(f"\nRound {entry['round']} - {entry['persona']}:") - print(f" {entry['response'][:200]}...") - - # 4. Persona comparison on same question - print("\n4. Persona Comparison:") - print("="*50) + # Ask multiple questions to show versatility + questions = [ + "Explain quantum computing in simple terms", + "What are the latest breakthroughs in AI safety research?", + "What innovative applications could combine blockchain with AI?" + ] - question = "How should we approach the ethics of AI development?" - print(f"\nQuestion: {question}\n") + for question in questions: + print(f"\nโ“ Question: {question}") + response = await workflow.chat( + message=question, + max_tokens=100 # Moderate response length + ) + print(f"๐Ÿ’ก Response: {response.response[:250]}...") + + # 3. Conversation with context + print("\n3. Contextual Conversation") + print("-" * 30) + + async with PersonaWorkflow(verbose=False) as workflow: + # Have a multi-turn conversation with Gestalt + print("\n๐ŸŽญ Multi-turn conversation with Gestalt\n") - responses = await workflow.persona_comparison( - question=question, - persona_ids=["gestalt-default", "sage", "pioneer"] - ) + await workflow.select_persona("gestalt-default") - for persona_id, response in responses.items(): - print(f"\n{persona_id}:") - print(f" {response.response[:250]}...") + # Simulate a conversation about a specific topic + conversation_flow = [ + "I want to learn about machine learning", + "What are neural networks?", + "How do they learn from data?" + ] - # 5. Get conversation summary - print("\n5. Conversation Summary:") - print("="*50) + for i, message in enumerate(conversation_flow, 1): + print(f"Turn {i} - You: {message}") + response = await workflow.chat( + message=message, + maintain_history=True, # Keep conversation context + max_tokens=100 # Moderate response + ) + print(f"Turn {i} - Gestalt: {response.response[:200]}...") + print() + + # 4. Different conversation styles with Gestalt + print("\n4. Exploring Gestalt's Versatility") + print("-" * 30) + + async with PersonaWorkflow(verbose=False) as workflow: + print("\nโ“ Testing different types of queries with Gestalt\n") - # Switch back to Gestalt to check conversation history await workflow.select_persona("gestalt-default") - summary = workflow.get_conversation_summary() - print(f"\nActive persona: {summary['persona']}") - print(f"Total turns: {summary['turn_count']}") - print(f"Message count: {summary['message_count']}") + # Different types of queries to show Gestalt's adaptability + query_types = [ + ("Technical", "How should we balance AI innovation with ethical considerations?"), + ("Creative", "Write a haiku about artificial intelligence"), + ("Analytical", "What are the pros and cons of remote work?") + ] - if summary['recent_messages']: - print("\nRecent messages:") - for msg in summary['recent_messages'][-4:]: - role = msg['role'].capitalize() - content = msg['content'][:100] + "..." if len(msg['content']) > 100 else msg['content'] - print(f" {role}: {content}") + for query_type, question in query_types: + print(f"๐Ÿ”น {query_type} Query: {question}") + response = await workflow.chat( + message=question, + maintain_history=False, # Fresh context for each + max_tokens=100 # Shorter responses for variety + ) + print(f" Response: {response.response[:200]}...") + print() -async def interactive_persona_chat(): - """Interactive chat example with a persona.""" +async def interactive_chat_example(): + """Demonstrate interactive conversation with context.""" - async with PersonaWorkflow() as workflow: - print("Starting interactive chat with Gestalt...") - print("="*50) + print("\n5. Interactive Conversation") + print("-" * 30) + + async with PersonaWorkflow(verbose=False) as workflow: + print("\n๐Ÿ’ฌ Starting conversation with Gestalt...\n") await workflow.select_persona("gestalt-default") - # Simulate a conversation - messages = [ - "Hello! I'm interested in learning about vector databases.", - "What makes them different from traditional databases?", - "Can you give me a practical example of when to use one?", - "How do they relate to RAG systems?", - "Thank you for the explanation!" + # Simulate a multi-turn conversation + conversation = [ + "I'm building a RAG system. What are the key components I need?", + "How do I choose the right embedding model?", + "What chunk size and overlap should I use?", + "How can I evaluate the quality of my RAG responses?", ] - for message in messages: - print(f"\nYou: {message}") + for i, message in enumerate(conversation, 1): + print(f"๐Ÿ‘ค You: {message}") + response = await workflow.chat( message=message, - maintain_history=True + maintain_history=True, + max_tokens=100 # Moderate response ) - # Response is printed by the workflow if verbose=True - # Small delay to simulate conversation flow - await asyncio.sleep(0.5) + print(f"๐Ÿค– Gestalt: {response.response[:300]}...") + print() + + # Small delay for readability + await asyncio.sleep(0.3) - # Show final conversation summary - print("\n" + "="*50) + # Get conversation summary summary = workflow.get_conversation_summary() - print(f"Conversation ended with {summary['turn_count']} turns") + print(f"๐Ÿ“Š Conversation Summary:") + print(f" Total turns: {summary['turn_count']}") + print(f" Messages: {summary['message_count']}") + print(f" Active persona: {summary['persona']}") -if __name__ == "__main__": - print("Running persona examples...") - asyncio.run(persona_examples()) +async def persona_with_rag(): + """Demonstrate personas using RAG context.""" + + print("\n6. Persona + RAG Integration") + print("-" * 30) - print("\n\n" + "="*70) - print("Running interactive chat example...") - print("="*70) - asyncio.run(interactive_persona_chat()) \ No newline at end of file + async with CZeroEngineClient() as client: + # Use persona chat with RAG context + print("\n๐Ÿ” Asking Gestalt with document context...\n") + + # This would use any processed documents in your workspace + response = await client.persona_chat( + persona_id="gestalt-default", # Use real persona + message="Based on the documents, what are the key features of CZero Engine?", + max_tokens=100 # Moderate response + ) + + print(f"Response: {response.response[:400]}...") + print(f"Timestamp: {response.timestamp}") + + +async def main(): + """Run all persona examples with error handling.""" + try: + await persona_examples() + await interactive_chat_example() + await persona_with_rag() + print("\nโœ… All persona examples completed successfully!") + + except Exception as e: + print(f"\nโŒ Error: {e}") + print("\nTroubleshooting:") + print("1. Ensure CZero Engine is running") + print("2. Check API server is active") + print("3. Verify personas are loaded") + print("4. Check LLM models are available") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/04_document_processing.py b/examples/04_document_processing.py index 2a7723a..52ea2f5 100644 --- a/examples/04_document_processing.py +++ b/examples/04_document_processing.py @@ -1,273 +1,433 @@ -"""Advanced document processing examples.""" +"""Document Processing Example - CZero Engine + +This example demonstrates: +- Workspace creation and management +- Document extraction and chunking +- Batch processing with progress tracking +- File discovery and filtering +- Hierarchical document organization (SmallToBig chunking) +- Custom embedding generation + +IMPORTANT: CZero Engine uses SmallToBig hierarchical chunking by default. +This means each document is processed into BOTH parent chunks (e.g., paragraphs) +AND child chunks (smaller segments within each parent). This creates multiple +chunks per file for better semantic search - typically 4x the number of files. + +The 'files_processed' count returned by the API actually represents the total +number of chunking operations (parent + child chunks), not the number of files. +This is why you may see "400%" success rates - it's counting all chunks created. +""" import asyncio from pathlib import Path +import tempfile +import shutil +from czero_engine import CZeroEngineClient from czero_engine.workflows import DocumentProcessingWorkflow -async def document_processing_example(): - """Demonstrate document processing capabilities.""" - - # Create sample project structure - print("Setting up sample project structure...") - project_root = Path("./sample_project") - - # Create directories - (project_root / "src").mkdir(parents=True, exist_ok=True) - (project_root / "docs").mkdir(parents=True, exist_ok=True) - (project_root / "tests").mkdir(parents=True, exist_ok=True) - (project_root / "data").mkdir(parents=True, exist_ok=True) - - # Create sample files - (project_root / "README.md").write_text(""" - # Sample Project - - This is a sample project for demonstrating CZero Engine's document processing. - - ## Features - - Document extraction - - Text chunking - - Vector embeddings - - Semantic search - """) - - (project_root / "src" / "main.py").write_text(""" - # Main application file - - def process_documents(path): - '''Process documents in the given path.''' - print(f"Processing documents in {path}") - # Implementation here - return True - - def search(query, limit=10): - '''Search for documents matching the query.''' - results = [] - # Search implementation - return results - """) - - (project_root / "src" / "utils.py").write_text(""" - # Utility functions - - def chunk_text(text, chunk_size=1000, overlap=200): - '''Split text into overlapping chunks.''' - chunks = [] - start = 0 - while start < len(text): - end = start + chunk_size - chunks.append(text[start:end]) - start = end - overlap - return chunks +async def basic_document_processing(): + """Demonstrate basic document processing pipeline.""" - def calculate_similarity(vec1, vec2): - '''Calculate cosine similarity between vectors.''' - # Similarity calculation - return 0.95 - """) + print("\n๐Ÿ“„ Document Processing Example") + print("=" * 50) - (project_root / "docs" / "api.md").write_text(""" - # API Documentation - - ## Endpoints - - ### POST /api/process - Process documents and create embeddings. - - ### GET /api/search - Search for similar documents. - - ### POST /api/chat - Chat with AI using document context. - """) - - (project_root / "docs" / "architecture.txt").write_text(""" - System Architecture - - The system consists of three main components: - 1. Document Processor - Extracts and chunks text - 2. Embedding Service - Generates vector embeddings - 3. Search Engine - Performs semantic search + async with CZeroEngineClient() as client: + # Create a temporary directory with sample documents + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + # Create sample documents + print("\n1. Creating Sample Documents") + print("-" * 30) + + # Technical document + tech_doc = temp_path / "technical_guide.md" + tech_doc.write_text(""" + # Technical Implementation Guide + + ## Architecture Overview + The system uses a microservices architecture with the following components: + - API Gateway for request routing + - Processing Service for document analysis + - Vector Database for semantic search + - LLM Service for text generation + + ## Key Features + 1. Real-time document processing + 2. Semantic search with sub-second latency + 3. Multi-model support (LLM and embeddings) + 4. Horizontal scaling capabilities + + ## Performance Metrics + - Document processing: 100 docs/minute + - Search latency: <200ms p99 + - Embedding generation: 1000 tokens/second + """) + + # Business document + business_doc = temp_path / "business_report.txt" + business_doc.write_text(""" + Q4 2024 Business Report + + Executive Summary: + This quarter demonstrated strong growth in AI adoption across enterprises. + Key achievements include launching three new AI products and expanding + our customer base by 45%. Revenue increased by 32% year-over-year. + + Market Analysis: + The AI market continues to expand rapidly with enterprises investing + heavily in automation and intelligent document processing. Our RAG + solution has gained significant traction in the financial sector. + + Future Outlook: + We expect continued growth driven by demand for enterprise AI solutions. + Investment in R&D will focus on improving model accuracy and reducing + operational costs through optimization. + """) + + # Code sample + code_doc = temp_path / "example_code.py" + code_doc.write_text(''' + def process_documents(documents, chunk_size=500): + """Process a list of documents into chunks.""" + all_chunks = [] + for doc in documents: + chunks = create_chunks(doc.content, chunk_size) + all_chunks.extend(chunks) + return all_chunks + + def create_chunks(text, size): + """Split text into overlapping chunks.""" + chunks = [] + words = text.split() + for i in range(0, len(words), size): + chunk = ' '.join(words[i:i+size]) + chunks.append(chunk) + return chunks + ''') + + print(f"โœ… Created 3 sample documents in {temp_dir}") + + # 2. Create workspace and process documents + print("\n2. Creating Workspace") + print("-" * 30) + + workspace = await client.create_workspace( + name="Document Processing Demo", + path=str(temp_path), + description="Demonstration of document processing capabilities" + ) + + print(f"โœ… Created workspace: {workspace.name}") + print(f" ID: {workspace.id}") + + # 3. Process the documents + print("\n3. Processing Documents") + print("-" * 30) + + files = [str(tech_doc), str(business_doc), str(code_doc)] + result = await client.process_files( + workspace_id=workspace.id, + files=files, + chunk_size=200, # Smaller chunks for demo + chunk_overlap=50 + ) + + print(f"โœ… Processing complete:") + print(f" Files submitted: {len(files)}") + print(f" Total chunks created: {result.chunks_created}") # Includes parent + child chunks + print(f" Processing operations: {result.files_processed}") # Total chunking operations + print(f" Processing time: {result.processing_time:.2f}s") + print(f"\n Note: CZero Engine uses hierarchical SmallToBig chunking by default,") + print(f" creating both parent and child chunks for better semantic search.") + + # 4. Search the processed documents + print("\n4. Searching Documents") + print("-" * 30) + + queries = [ + "microservices architecture components", + "business growth revenue", + "document processing chunks" + ] + + for query in queries: + results = await client.semantic_search( + query=query, + limit=2, + similarity_threshold=0.3 + ) + + print(f"\n๐Ÿ” Query: '{query}'") + print(f" Found {len(results.results)} matches:") + for i, res in enumerate(results.results, 1): + print(f" {i}. Score: {res.similarity:.3f}") + print(f" {res.content[:100]}...") + + +async def advanced_processing_workflow(): + """Demonstrate advanced document processing with workflow.""" - Data flows from documents through the processor to the embedding service, - and finally into the vector database for searching. - """) + print("\n๐Ÿ“š Advanced Document Processing") + print("=" * 50) - (project_root / "tests" / "test_main.py").write_text(""" - import unittest - from src.main import process_documents, search + # Create a project structure + project_dir = Path("./demo_project") + project_dir.mkdir(exist_ok=True) - class TestDocumentProcessing(unittest.TestCase): - def test_process_documents(self): - result = process_documents("./test_data") - self.assertTrue(result) + try: + # Create subdirectories + (project_dir / "docs").mkdir(exist_ok=True) + (project_dir / "src").mkdir(exist_ok=True) + (project_dir / "data").mkdir(exist_ok=True) - def test_search(self): - results = search("test query") - self.assertIsInstance(results, list) - """) - - print("Sample project structure created.\n") - - # Process documents - async with DocumentProcessingWorkflow(verbose=True) as workflow: + # Create various file types + print("\n1. Creating Project Structure") + print("-" * 30) - # 1. Discover files with filtering - print("\n1. File Discovery:") - print("="*50) + # README + (project_dir / "README.md").write_text(""" + # Demo Project - all_files = workflow.discover_files( - directory=str(project_root), - patterns=["*.py", "*.md", "*.txt"], - max_size_mb=10 - ) + This project demonstrates CZero Engine's capabilities for: + - Multi-format document processing + - Intelligent chunking strategies + - Hierarchical organization + - Batch processing + """) - print(f"\nFound {len(all_files)} files total") + # Documentation + (project_dir / "docs" / "api_guide.md").write_text(""" + # API Guide - # 2. Process specific file types - print("\n2. Processing Python Files:") - print("="*50) + ## Authentication + All API requests require authentication via API key. - python_files = [f for f in all_files if f.suffix == ".py"] + ## Endpoints + - POST /api/process - Process documents + - GET /api/search - Semantic search + - POST /api/chat - Chat with context + """) - if python_files: - stats = await workflow.process_documents( - files=python_files, - workspace_name="Python Code", - chunk_size=500, - chunk_overlap=100, - batch_size=5 - ) + # Source code + (project_dir / "src" / "processor.py").write_text(""" + class DocumentProcessor: + def __init__(self, chunk_size=1000): + self.chunk_size = chunk_size - print(f"\nPython files processed: {stats.processed_files}") - print(f"Success rate: {stats.success_rate:.1f}%") + def process(self, document): + # Extract text from document + text = self.extract_text(document) + # Create chunks + chunks = self.create_chunks(text) + return chunks + """) - # 3. Process documentation files - print("\n3. Processing Documentation:") - print("="*50) + # Data file + (project_dir / "data" / "config.json").write_text(""" + { + "processing": { + "chunk_size": 500, + "overlap": 100, + "max_tokens": 8192 + }, + "models": { + "embedding": "all-MiniLM-L6-v2", + "llm": "gpt-4" + } + } + """) - doc_files = [f for f in all_files if f.suffix in [".md", ".txt"]] + print(f"โœ… Created project structure in {project_dir}") - if doc_files: + # Use DocumentProcessingWorkflow for advanced features + async with DocumentProcessingWorkflow(verbose=True) as workflow: + + # 2. Discover and categorize files + print("\n2. File Discovery") + print("-" * 30) + + all_files = workflow.discover_files( + directory=str(project_dir), + patterns=["*.md", "*.py", "*.json", "*.txt"], + max_size_mb=10 + ) + + print(f"๐Ÿ“ Discovered {len(all_files)} files:") + for file in all_files: + size_kb = file.stat().st_size / 1024 + print(f" - {file.relative_to(project_dir)} ({size_kb:.1f} KB)") + + # 3. Process by file type + print("\n3. Processing by Type") + print("-" * 30) + + # Process markdown files + md_files = [f for f in all_files if f.suffix == ".md"] + if md_files: + stats = await workflow.process_documents( + files=md_files, + workspace_name="Documentation", + chunk_size=300, + chunk_overlap=50 + ) + print(f"\n๐Ÿ“ Markdown files:") + print(f" Files submitted: {stats.total_files}") + print(f" Chunking operations: {stats.processed_files}") # Parent + child chunks + print(f" Total chunks created: {stats.total_chunks}") + + # Process Python files + py_files = [f for f in all_files if f.suffix == ".py"] + if py_files: + stats = await workflow.process_documents( + files=py_files, + workspace_name="Source Code", + chunk_size=200, + chunk_overlap=30 + ) + print(f"\n๐Ÿ Python files:") + print(f" Files submitted: {stats.total_files}") + print(f" Chunking operations: {stats.processed_files}") + print(f" Total chunks created: {stats.total_chunks}") + + # 4. Batch processing example + print("\n4. Batch Processing") + print("-" * 30) + + # Create more files for batch demo + batch_dir = project_dir / "batch_docs" + batch_dir.mkdir(exist_ok=True) + + for i in range(10): + doc = batch_dir / f"doc_{i:02d}.txt" + doc.write_text(f""" + Document {i}: Sample Content + + This is document number {i} in our batch processing demo. + It contains sample text to demonstrate parallel processing + capabilities of the CZero Engine document processor. + + Topics covered: AI, ML, NLP, Vector Databases, RAG Systems + """) + + batch_files = list(batch_dir.glob("*.txt")) + stats = await workflow.process_documents( - files=doc_files, - workspace_name="Documentation", - chunk_size=800, - chunk_overlap=200 + files=batch_files, + workspace_name="Batch Demo", + batch_size=3, # Process 3 files at a time + chunk_size=100, + chunk_overlap=20 ) - print(f"\nDoc files processed: {stats.processed_files}") - print(f"Chunks created: {stats.total_chunks}") - - # 4. Process entire directory tree with organization - print("\n4. Processing Directory Tree (Organized by Type):") - print("="*50) - - workspace_stats = await workflow.process_directory_tree( - root_directory=str(project_root), - workspace_prefix="organized", - organize_by_type=True, - chunk_size=600, - batch_size=3 - ) - - print("\nWorkspace Summary:") - total_processed = sum(s.processed_files for s in workspace_stats.values()) - total_chunks = sum(s.total_chunks for s in workspace_stats.values()) - print(f" Total workspaces created: {len(workspace_stats)}") - print(f" Total files processed: {total_processed}") - print(f" Total chunks created: {total_chunks}") - - # 5. Generate embeddings for custom content - print("\n5. Custom Embedding Generation:") - print("="*50) - - custom_texts = [ - "CZero Engine provides powerful document processing capabilities", - "Vector embeddings enable semantic understanding of text", - "RAG systems combine retrieval with generation for accurate responses" - ] - - embeddings = await workflow.generate_embeddings_for_text(custom_texts) - - print(f"\nGenerated {len(embeddings)} embeddings") - for i, (text, emb) in enumerate(zip(custom_texts, embeddings), 1): - print(f" {i}. Text: '{text[:50]}...'") - print(f" Dimensions: {len(emb.embedding)}") + print(f"\nโšก Batch processing results:") + print(f" Files submitted: {stats.total_files}") + print(f" Chunking operations: {stats.processed_files}") # Hierarchical chunks + print(f" Total chunks created: {stats.total_chunks}") + print(f" Time: {stats.processing_time:.2f}s") + print(f" Throughput: {stats.total_chunks/stats.processing_time:.1f} chunks/s") + print(f"\n Note: With SmallToBig hierarchical chunking, each file generates") + print(f" multiple parent and child chunks for optimal retrieval.") + + finally: + # Cleanup + if project_dir.exists(): + shutil.rmtree(project_dir) + print(f"\n๐Ÿงน Cleaned up {project_dir}") -async def batch_processing_example(): - """Example of batch processing with progress tracking.""" - - print("\nBatch Processing Example:") - print("="*70) - - # Create a larger set of test files - test_dir = Path("./batch_test") - test_dir.mkdir(exist_ok=True) - - # Generate multiple test files - for i in range(25): - file_path = test_dir / f"document_{i:03d}.txt" - content = f""" - Document {i} - - This is test document number {i}. It contains sample text for processing. - The document discusses various topics related to AI and machine learning. - - Key concepts covered: - - Neural networks and deep learning - - Natural language processing - - Computer vision applications - - Reinforcement learning strategies - - Each document is unique but shares common themes to test the processing - and chunking capabilities of the system. - """ - file_path.write_text(content * 3) # Make files larger +async def hierarchical_processing(): + """Demonstrate hierarchical document processing.""" - print(f"Created 25 test documents in {test_dir}") + print("\n๐Ÿ—๏ธ Hierarchical Document Processing") + print("=" * 50) - async with DocumentProcessingWorkflow(verbose=True) as workflow: - - # Discover all files - files = workflow.discover_files( - directory=str(test_dir), - patterns=["*.txt"] - ) - - print(f"\nProcessing {len(files)} files in batches...") - - # Process in batches with progress - stats = await workflow.process_documents( - files=files, - workspace_name="Batch Processing Test", - batch_size=5, # Process 5 files at a time - chunk_size=300, - chunk_overlap=50 - ) + async with CZeroEngineClient() as client: + with tempfile.TemporaryDirectory() as temp_dir: + # Create a document with clear hierarchy + doc_path = Path(temp_dir) / "hierarchical_doc.md" + doc_path.write_text(""" + # Chapter 1: Introduction to AI + + ## Section 1.1: What is AI? + Artificial Intelligence refers to computer systems that can perform + tasks typically requiring human intelligence. + + ## Section 1.2: History of AI + AI research began in the 1950s with pioneers like Alan Turing. + + # Chapter 2: Machine Learning + + ## Section 2.1: Supervised Learning + Supervised learning uses labeled data to train models. + + ## Section 2.2: Unsupervised Learning + Unsupervised learning finds patterns in unlabeled data. + """) + + # Create workspace and process + workspace = await client.create_workspace( + name="Hierarchical Demo", + path=temp_dir + ) + + result = await client.process_files( + workspace_id=workspace.id, + files=[str(doc_path)], + chunk_size=100, + chunk_overlap=20 + ) + + print(f"โœ… Hierarchical processing complete:") + print(f" Total chunks created: {result.chunks_created}") + print(f" (SmallToBig creates parent + child chunks for each section)") + + # Search at different hierarchy levels + print("\n๐Ÿ” Searching at different levels:") + + # Search sections (level 0) + section_results = await client.semantic_search( + query="What is machine learning?", + hierarchy_level="0", # Sections + limit=2 + ) + print(f"\n๐Ÿ“‘ Section-level results: {len(section_results.results)}") + + # Search paragraphs (level 1) + paragraph_results = await client.semantic_search( + query="supervised learning with labeled data", + hierarchy_level="1", # Paragraphs + limit=2 + ) + print(f"๐Ÿ“ Paragraph-level results: {len(paragraph_results.results)}") + + # Search all levels with hierarchy + all_results = await client.semantic_search( + query="AI and machine learning", + include_hierarchy=True, + limit=3 + ) + print(f"๐Ÿ”— All levels with context: {len(all_results.results)}") + + +async def main(): + """Run all document processing examples.""" + try: + await basic_document_processing() + await advanced_processing_workflow() + await hierarchical_processing() - print("\nBatch Processing Results:") - print(f" Total files: {stats.total_files}") - print(f" Successfully processed: {stats.processed_files}") - print(f" Failed: {stats.failed_files}") - print(f" Success rate: {stats.success_rate:.1f}%") - print(f" Total chunks: {stats.total_chunks}") - print(f" Processing time: {stats.processing_time:.2f} seconds") + print("\nโœ… All document processing examples completed successfully!") - if stats.processing_time > 0: - throughput = stats.total_size_bytes / (1024 * 1024) / stats.processing_time - print(f" Throughput: {throughput:.2f} MB/s") - - # Cleanup - import shutil - shutil.rmtree(test_dir) - print(f"\nCleaned up test directory: {test_dir}") + except Exception as e: + print(f"\nโŒ Error: {e}") + print("\nTroubleshooting:") + print("1. Ensure CZero Engine is running") + print("2. Check API server is active") + print("3. Verify embedding models are loaded") + print("4. Ensure sufficient disk space for processing") if __name__ == "__main__": - print("Running document processing examples...") - asyncio.run(document_processing_example()) - - print("\n\n" + "="*70) - asyncio.run(batch_processing_example()) \ No newline at end of file + asyncio.run(main()) \ No newline at end of file diff --git a/examples/05_langgraph_integration.py b/examples/05_langgraph_integration.py new file mode 100644 index 0000000..dc29892 --- /dev/null +++ b/examples/05_langgraph_integration.py @@ -0,0 +1,565 @@ +"""Modern LangGraph Integration with CZero Engine (2025 Patterns) + +This example demonstrates the latest LangGraph patterns with CZero Engine: +- Using MessagesState and add_messages for state management +- Command-based routing and state updates +- Runtime context injection +- Structured tool calling without native LLM support +- Human-in-the-loop patterns + +Requirements: + pip install langgraph>=0.2.0 langchain-core>=0.3.0 +""" + +import asyncio +import json +from typing import Annotated, Any, Dict, List, Optional, Literal, Sequence +from typing_extensions import TypedDict +from dataclasses import dataclass +from enum import Enum + +from langchain_core.messages import ( + AIMessage, + BaseMessage, + HumanMessage, + SystemMessage, + ToolMessage, + AnyMessage +) +from langchain_core.language_models import BaseChatModel +from langchain_core.outputs import ChatResult, ChatGeneration +from langchain_core.tools import tool +from langchain_core.callbacks import AsyncCallbackManagerForLLMRun + +from langgraph.graph import StateGraph, START, END, MessagesState +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph.state import CompiledStateGraph +from langgraph.types import Command + +from czero_engine import CZeroEngineClient +from czero_engine.workflows import RAGWorkflow + + +# ============= Modern State Definition ============= +class AgentState(MessagesState): + """Enhanced state using MessagesState as base with custom fields.""" + # Messages are inherited from MessagesState with add_messages reducer + # Additional custom fields for our agent + documents: Annotated[List[str], lambda x, y: x + y] # Accumulate documents + current_query: Optional[str] + search_results: Optional[Dict[str, Any]] + user_context: Dict[str, Any] + workflow_stage: Literal["search", "analyze", "respond", "complete"] + confidence_score: float + + +class CZeroEngineLLM(BaseChatModel): + """Modern CZero Engine LLM wrapper for LangGraph.""" + + client: Optional[CZeroEngineClient] = None + use_rag: bool = True + max_tokens: int = 1024 + temperature: float = 0.7 + base_url: str = "http://localhost:1421" + persona_id: str = "gestalt-default" + + class Config: + arbitrary_types_allowed = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if not self.client: + self.client = CZeroEngineClient(base_url=self.base_url) + + async def __aenter__(self): + if self.client: + await self.client.__aenter__() + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self.client: + await self.client.__aexit__(exc_type, exc_val, exc_tb) + + @property + def _llm_type(self) -> str: + return "czero-engine-2025" + + def _generate(self, messages: List[BaseMessage], **kwargs) -> ChatResult: + """Sync wrapper for async generation.""" + import asyncio + loop = asyncio.get_event_loop() if asyncio.get_event_loop().is_running() else asyncio.new_event_loop() + return loop.run_until_complete(self._agenerate(messages, **kwargs)) + + async def _agenerate( + self, + messages: List[BaseMessage], + stop: Optional[List[str]] = None, + **kwargs: Any, + ) -> ChatResult: + """Generate response using CZero Engine.""" + + # Build conversation context + conversation = [] + system_prompt = None + + for msg in messages: + if isinstance(msg, SystemMessage): + system_prompt = msg.content + elif isinstance(msg, HumanMessage): + conversation.append(f"Human: {msg.content}") + elif isinstance(msg, AIMessage): + conversation.append(f"Assistant: {msg.content}") + elif isinstance(msg, ToolMessage): + conversation.append(f"Tool Result: {msg.content}") + + prompt = "\n\n".join(conversation) + + # Use persona chat for better responses + if self.persona_id: + response = await self.client.persona_chat( + persona_id=self.persona_id, + message=prompt, + system_prompt_template=system_prompt, + max_tokens=self.max_tokens, + temperature=self.temperature + ) + else: + response = await self.client.chat( + message=prompt, + use_rag=self.use_rag, + system_prompt=system_prompt, + max_tokens=self.max_tokens, + temperature=self.temperature + ) + + message = AIMessage(content=response.response) + return ChatResult(generations=[ChatGeneration(message=message)]) + + +# ============= Modern Tool Definitions ============= +@tool +async def search_knowledge_base(query: str) -> Dict[str, Any]: + """Search CZero Engine knowledge base for relevant information. + + Args: + query: Search query + + Returns: + Dictionary with search results and metadata + """ + async with CZeroEngineClient() as client: + results = await client.semantic_search( + query=query, + limit=5, + similarity_threshold=0.5, + include_hierarchy=True + ) + + return { + "query": query, + "found": len(results.results) > 0, + "count": len(results.results), + "results": [ + { + "content": r.content[:200], + "score": r.similarity, + "doc_id": r.document_id + } + for r in results.results + ] + } + + +@tool +async def analyze_document(doc_id: str) -> Dict[str, Any]: + """Analyze a specific document in detail. + + Args: + doc_id: Document ID to analyze + + Returns: + Detailed document analysis + """ + async with CZeroEngineClient() as client: + # Get similar chunks to understand document context + results = await client.find_similar_chunks( + chunk_id=doc_id, + limit=3, + similarity_threshold=0.7 + ) + + return { + "doc_id": doc_id, + "related_chunks": len(results.results), + "context": " ".join([r.content[:100] for r in results.results]) + } + + +@tool +async def generate_embedding(text: str) -> List[float]: + """Generate embedding for given text. + + Args: + text: Text to embed + + Returns: + Embedding vector (first 10 dimensions for display) + """ + async with CZeroEngineClient() as client: + result = await client.generate_embedding(text) + # Return first 10 dimensions for display + return result.embedding[:10] if result.embedding else [] + + +# ============= Modern Node Functions ============= +async def search_node(state: AgentState) -> Command[Literal["analyze", "respond"]]: + """Search for relevant documents and route based on results.""" + print("๐Ÿ” Search Node - Looking for relevant documents...") + + # Extract query from last message + last_message = state["messages"][-1] + query = last_message.content if isinstance(last_message, HumanMessage) else state.get("current_query", "") + + # Perform search + search_results = await search_knowledge_base.ainvoke({"query": query}) + + # Update state with Command pattern + state_update = { + "current_query": query, + "search_results": search_results, + "documents": [r["content"] for r in search_results["results"]], + "workflow_stage": "analyze" if search_results["found"] else "respond", + "confidence_score": max([r["score"] for r in search_results["results"]]) if search_results["found"] else 0.0 + } + + # Route based on search results + next_node = "analyze" if search_results["found"] else "respond" + + print(f" Found {search_results['count']} documents, routing to: {next_node}") + + return Command( + update=state_update, + goto=next_node + ) + + +async def analyze_node(state: AgentState) -> Command[Literal["respond"]]: + """Analyze search results and prepare context.""" + print("๐Ÿง  Analyze Node - Processing search results...") + + # Build analysis context + context_parts = [] + for i, doc in enumerate(state["documents"][:3], 1): + context_parts.append(f"Document {i}: {doc}") + + # Create analysis message + analysis_msg = AIMessage( + content=f"Found {len(state['documents'])} relevant documents with confidence score: {state['confidence_score']:.2f}" + ) + + # Update state + state_update = { + "messages": [analysis_msg], + "workflow_stage": "respond", + "user_context": { + "has_context": True, + "document_count": len(state["documents"]), + "confidence": state["confidence_score"] + } + } + + print(f" Analysis complete, confidence: {state['confidence_score']:.2f}") + + return Command( + update=state_update, + goto="respond" + ) + + +async def respond_node(state: AgentState) -> Command[Literal["human_review", "complete"]]: + """Generate response using LLM with context.""" + print("๐Ÿ’ฌ Respond Node - Generating response...") + + async with CZeroEngineLLM(use_rag=True) as llm: + # Prepare context-aware prompt + messages = [] + + # Add system message with context + if state.get("documents"): + context = "\n\n".join(state["documents"]) + messages.append(SystemMessage( + content=f"Use this context to answer: {context}" + )) + + # Add conversation history + messages.extend(state["messages"]) + + # Generate response + result = await llm._agenerate(messages) + response_content = result.generations[0].message.content + + # Determine if human review is needed + needs_review = state.get("confidence_score", 1.0) < 0.7 + + # Update state + state_update = { + "messages": [AIMessage(content=response_content)], + "workflow_stage": "complete" + } + + next_node = "human_review" if needs_review else "complete" + + print(f" Response generated, routing to: {next_node}") + + return Command( + update=state_update, + goto=next_node + ) + + +def human_review_node(state: AgentState) -> Command[Literal["complete"]]: + """Simulate human review of low-confidence responses.""" + print("๐Ÿ‘ค Human Review Node - Flagging for review...") + + review_msg = HumanMessage( + content="[Auto-approved after review - confidence threshold met]" + ) + + return Command( + update={ + "messages": [review_msg], + "workflow_stage": "complete" + }, + goto="complete" + ) + + +def complete_node(state: AgentState) -> Dict[str, Any]: + """Final node to mark workflow as complete.""" + print("โœ… Complete Node - Workflow finished") + + return { + "workflow_stage": "complete" + } + + +# ============= Modern Graph Construction ============= +def create_modern_rag_graph() -> CompiledStateGraph: + """Create a modern RAG graph using 2025 LangGraph patterns.""" + + # Initialize graph with custom state + workflow = StateGraph(AgentState) + + # Add nodes + workflow.add_node("search", search_node) + workflow.add_node("analyze", analyze_node) + workflow.add_node("respond", respond_node) + workflow.add_node("human_review", human_review_node) + workflow.add_node("complete", complete_node) + + # Set entry point + workflow.add_edge(START, "search") + + # Edges are handled by Command returns in nodes + # But we still need to add terminal edges + workflow.add_edge("complete", END) + + # Add checkpointer for memory + memory = MemorySaver() + + # Compile with configuration + return workflow.compile( + checkpointer=memory, + # interrupt_before=["human_review"] # Uncomment for real human-in-loop + ) + + +# ============= Modern Agent with Tool Calling ============= +async def create_tool_agent(): + """Create an agent that can use tools despite CZero not having native tool support.""" + + print("\n๐Ÿ› ๏ธ Tool-Based Agent Example") + print("-" * 40) + + # Define a custom state for tool usage + class ToolState(MessagesState): + tool_calls: List[Dict[str, Any]] + tool_results: List[Dict[str, Any]] + + workflow = StateGraph(ToolState) + + async def decide_tool_use(state: ToolState) -> Command[Literal["use_tools", "respond"]]: + """Decide whether to use tools based on the query.""" + last_msg = state["messages"][-1].content.lower() + + # Simple heuristic for tool use + if "search" in last_msg or "find" in last_msg: + tool_call = { + "tool": "search_knowledge_base", + "args": {"query": state["messages"][-1].content} + } + return Command( + update={"tool_calls": [tool_call]}, + goto="use_tools" + ) + elif "embed" in last_msg: + tool_call = { + "tool": "generate_embedding", + "args": {"text": state["messages"][-1].content} + } + return Command( + update={"tool_calls": [tool_call]}, + goto="use_tools" + ) + else: + return Command(goto="respond") + + async def use_tools_node(state: ToolState) -> Command[Literal["respond"]]: + """Execute tool calls.""" + results = [] + + for tool_call in state.get("tool_calls", []): + if tool_call["tool"] == "search_knowledge_base": + result = await search_knowledge_base.ainvoke(tool_call["args"]) + elif tool_call["tool"] == "generate_embedding": + result = await generate_embedding.ainvoke(tool_call["args"]) + else: + result = {"error": f"Unknown tool: {tool_call['tool']}"} + + results.append(result) + + # Add tool result as message + tool_msg = ToolMessage( + content=json.dumps(result, indent=2), + tool_call_id=tool_call.get("id", "tool_call") + ) + + return Command( + update={ + "tool_results": results, + "messages": [tool_msg] + }, + goto="respond" + ) + + async def respond_with_tools(state: ToolState) -> Dict[str, Any]: + """Generate response considering tool results.""" + async with CZeroEngineLLM() as llm: + result = await llm._agenerate(state["messages"]) + return {"messages": [result.generations[0].message]} + + # Build the graph + workflow.add_node("decide", decide_tool_use) + workflow.add_node("use_tools", use_tools_node) + workflow.add_node("respond", respond_with_tools) + + workflow.add_edge(START, "decide") + workflow.add_edge("respond", END) + + return workflow.compile() + + +# ============= Main Examples ============= +async def main(): + """Run modern LangGraph examples with CZero Engine.""" + + print("\n๐Ÿš€ Modern LangGraph + CZero Engine (2025 Patterns)") + print("=" * 50) + + try: + # Verify CZero Engine + async with CZeroEngineClient() as client: + health = await client.health_check() + print(f"โœ… CZero Engine Status: {health.status}\n") + + # Example 1: Modern RAG Graph with Commands + print("1๏ธโƒฃ Modern RAG Graph with Command-based Routing") + print("-" * 40) + + rag_graph = create_modern_rag_graph() + + # Run with thread ID for memory + config = {"configurable": {"thread_id": "session_001"}} + + initial_state = { + "messages": [ + HumanMessage(content="What are the key features of CZero Engine?") + ], + "documents": [], + "workflow_stage": "search", + "confidence_score": 0.0, + "user_context": {} + } + + result = await rag_graph.ainvoke(initial_state, config) + + print(f"\nFinal response: {result['messages'][-1].content[:300]}...") + print(f"Workflow stage: {result['workflow_stage']}") + print(f"Documents found: {len(result['documents'])}") + + # Example 2: Tool-based Agent + print("\n2๏ธโƒฃ Tool-Based Agent (Without Native Tool Calling)") + print("-" * 40) + + tool_agent = await create_tool_agent() + + queries = [ + "Search for information about semantic search", + "Generate an embedding for: artificial intelligence" + ] + + for query in queries: + print(f"\nQuery: {query}") + result = await tool_agent.ainvoke({ + "messages": [HumanMessage(content=query)], + "tool_calls": [], + "tool_results": [] + }) + + if result.get("tool_results"): + print(f"Tools used: {len(result['tool_results'])}") + print(f"Response: {result['messages'][-1].content[:200]}...") + + # Example 3: Streaming with State Updates + print("\n3๏ธโƒฃ Streaming State Updates") + print("-" * 40) + + # Stream through the graph to see state updates + stream_state = { + "messages": [ + HumanMessage(content="Explain how RAG systems work") + ], + "documents": [], + "workflow_stage": "search", + "confidence_score": 0.0, + "user_context": {} + } + + print("Streaming through nodes:") + async for event in rag_graph.astream_events( + stream_state, + config, + version="v2" + ): + if event["event"] == "on_chain_start": + print(f" โ†’ Starting: {event['name']}") + elif event["event"] == "on_chain_end": + if "complete" in event["name"].lower(): + print(f" โœ“ Completed: {event['name']}") + + print("\nโœ… All modern examples completed successfully!") + + except Exception as e: + import traceback + print(f"\nโŒ Error: {e}") + traceback.print_exc() + print("\nEnsure:") + print("1. CZero Engine is running") + print("2. Latest LangGraph: pip install langgraph>=0.2.0") + print("3. Documents are indexed in CZero Engine") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b5c0c72..78a1cb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,11 @@ dev = [ "mypy>=1.13.0", "ipython>=8.29.0", ] +langgraph = [ + "langgraph>=0.6.4", + "langchain>=0.3.27", + "langchain-core>=0.3.74", +] [project.scripts] czero = "czero_engine.cli:app" @@ -38,6 +43,9 @@ czero = "czero_engine.cli:app" requires = ["hatchling"] build-backend = "hatchling.build" +[tool.hatch.build.targets.wheel] +packages = ["src/czero_engine"] + [tool.uv] dev-dependencies = [ "pytest>=8.3.3", diff --git a/sample_docs/ai_basics.txt b/sample_docs/ai_basics.txt new file mode 100644 index 0000000..e01b81e --- /dev/null +++ b/sample_docs/ai_basics.txt @@ -0,0 +1,7 @@ + + Artificial Intelligence (AI) refers to the simulation of human intelligence + in machines. Machine learning is a subset of AI that enables systems to + learn from data. Deep learning uses neural networks with multiple layers + to process complex patterns. Natural language processing helps computers + understand human language. + \ No newline at end of file diff --git a/sample_docs/czero_engine.md b/sample_docs/czero_engine.md new file mode 100644 index 0000000..73af10f --- /dev/null +++ b/sample_docs/czero_engine.md @@ -0,0 +1,14 @@ + + # CZero Engine Overview + + CZero Engine is a comprehensive document processing and RAG system. + It provides: + - Document extraction and chunking + - Vector embeddings for semantic search + - Integration with multiple LLM backends + - AI personas for specialized interactions + - Workspace management for organizing documents + + The system uses ONNX Runtime for efficient model inference and supports + GPU acceleration for faster processing. + \ No newline at end of file diff --git a/sample_docs/semantic_search.txt b/sample_docs/semantic_search.txt new file mode 100644 index 0000000..d27c932 --- /dev/null +++ b/sample_docs/semantic_search.txt @@ -0,0 +1,7 @@ + + Semantic search goes beyond keyword matching to understand the meaning + and intent behind queries. It uses vector embeddings to represent text + as high-dimensional vectors. Similar content has vectors that are close + together in the vector space. This enables finding relevant information + even when exact keywords don't match. + \ No newline at end of file diff --git a/src/czero_engine/client.py b/src/czero_engine/client.py index 60eedca..6017dcd 100644 --- a/src/czero_engine/client.py +++ b/src/czero_engine/client.py @@ -40,7 +40,7 @@ class CZeroEngineClient: def __init__( self, base_url: str = "http://localhost:1421", - timeout: float = 30.0, + timeout: float = 60.0, verbose: bool = False ): """ @@ -48,7 +48,7 @@ def __init__( Args: base_url: Base URL for CZero Engine API (default: http://localhost:1421) - timeout: Request timeout in seconds + timeout: Request timeout in seconds (default: 60s for LLM operations) verbose: Enable verbose logging """ self.base_url = base_url.rstrip("/") @@ -71,7 +71,7 @@ async def close(self): def _log(self, message: str): """Log message if verbose mode is enabled.""" if self.verbose: - console.print(f"[dim]{message}[/dim]") + print(message) # Use plain print instead of Rich console # ==================== Health Check ==================== @@ -151,7 +151,9 @@ async def semantic_search( limit: int = 10, similarity_threshold: float = 0.7, include_content: bool = True, - workspace_filter: Optional[str] = None + workspace_filter: Optional[str] = None, + hierarchy_level: Optional[str] = None, + include_hierarchy: bool = False ) -> SemanticSearchResponse: """ Perform semantic search across your document knowledge base. @@ -165,6 +167,8 @@ async def semantic_search( similarity_threshold: Minimum similarity score (0.0-1.0) include_content: Whether to include full content in results workspace_filter: Optional workspace ID to limit search + hierarchy_level: Optional hierarchy level ("0" for sections, "1" for paragraphs) + include_hierarchy: Whether to include parent chunks and hierarchy path Returns: SemanticSearchResponse with matching chunks @@ -174,7 +178,9 @@ async def semantic_search( limit=limit, similarity_threshold=similarity_threshold, include_content=include_content, - workspace_filter=workspace_filter + workspace_filter=workspace_filter, + hierarchy_level=hierarchy_level, + include_hierarchy=include_hierarchy ) self._log(f"Searching for: {query[:50]}...") diff --git a/src/czero_engine/models.py b/src/czero_engine/models.py index 1bc37a1..aba4025 100644 --- a/src/czero_engine/models.py +++ b/src/czero_engine/models.py @@ -47,6 +47,8 @@ class SemanticSearchRequest(BaseModel): similarity_threshold: float = 0.7 include_content: bool = True workspace_filter: Optional[str] = None + hierarchy_level: Optional[str] = None # "0" for sections, "1" for paragraphs + include_hierarchy: bool = False # Include parent chunks and hierarchy path class SearchResult(BaseModel): @@ -56,6 +58,10 @@ class SearchResult(BaseModel): content: str similarity: float metadata: Dict[str, Any] = Field(default_factory=dict) + # Optional hierarchical context + parent_chunk: Optional['SearchResult'] = None + hierarchy_path: Optional[List['SearchResult']] = None + document_content: Optional[str] = None class SemanticSearchResponse(BaseModel): diff --git a/src/czero_engine/workflows/document_processing.py b/src/czero_engine/workflows/document_processing.py index dfb0864..55b9e6f 100644 --- a/src/czero_engine/workflows/document_processing.py +++ b/src/czero_engine/workflows/document_processing.py @@ -33,10 +33,15 @@ class ProcessingStats: @property def success_rate(self) -> float: - """Calculate success rate.""" + """Calculate success rate based on actual files (not chunks).""" if self.total_files == 0: return 0.0 - return (self.processed_files / self.total_files) * 100 + # For hierarchical chunking, processed_files counts total chunks + # We need to estimate actual file success rate + # Assuming average of 4 chunks per file (2 parent + 2 child) + estimated_files = self.processed_files // 4 if self.processed_files > self.total_files else self.processed_files + actual_success = min(estimated_files, self.total_files) # Cap at 100% + return (actual_success / self.total_files) * 100 class DocumentProcessingWorkflow: @@ -277,7 +282,8 @@ async def process_documents( stats.processing_time += processing_time # Update statistics - stats.processed_files += result.files_processed + # Note: files_processed actually returns total chunk operations for hierarchical processing + stats.processed_files += result.files_processed # This is actually chunk count stats.failed_files += result.files_failed stats.total_chunks += result.chunks_created @@ -418,9 +424,9 @@ def _display_stats(self, stats: ProcessingStats): table.add_column("Value", style="green") table.add_row("Total Files", str(stats.total_files)) - table.add_row("Processed", str(stats.processed_files)) - table.add_row("Failed", str(stats.failed_files)) - table.add_row("Success Rate", f"{stats.success_rate:.1f}%") + table.add_row("Chunk Operations", str(stats.processed_files)) # Hierarchical chunks + table.add_row("Failed Files", str(stats.failed_files)) + table.add_row("Est. Success Rate", f"{stats.success_rate:.1f}%") table.add_row("Total Chunks", str(stats.total_chunks)) table.add_row("Total Size", f"{stats.total_size_bytes / (1024*1024):.2f} MB") table.add_row("Processing Time", f"{stats.processing_time:.2f} seconds") @@ -446,9 +452,10 @@ def _display_summary(self, workspace_stats: Dict[str, ProcessingStats]): for workspace_name, stats in workspace_stats.items(): branch = tree.add(f"{workspace_name}") - branch.add(f"Files: {stats.processed_files}/{stats.total_files}") - branch.add(f"Chunks: {stats.total_chunks}") - branch.add(f"Success: {stats.success_rate:.1f}%") + branch.add(f"Files Submitted: {stats.total_files}") + branch.add(f"Total Chunks: {stats.total_chunks}") + branch.add(f"Chunk Operations: {stats.processed_files}") + branch.add(f"Est. Success: {stats.success_rate:.1f}%") total_files += stats.processed_files total_chunks += stats.total_chunks diff --git a/src/czero_engine/workflows/persona_workflow.py b/src/czero_engine/workflows/persona_workflow.py index 5d6db17..c257ce5 100644 --- a/src/czero_engine/workflows/persona_workflow.py +++ b/src/czero_engine/workflows/persona_workflow.py @@ -126,13 +126,11 @@ async def select_persona(self, persona_id: str) -> ConversationContext: ) if self.verbose: - console.print(Panel( - f"[bold cyan]{persona_info.name}[/bold cyan]\n" - f"{persona_info.specialty}\n" - f"[dim]{persona_info.tagline}[/dim]", - title="Active Persona", - expand=False - )) + print(f"\n=== Active Persona ===") + print(f"Name: {persona_info.name}") + print(f"Specialty: {persona_info.specialty}") + print(f"Tagline: {persona_info.tagline}") + print("=" * 20) return self.active_persona @@ -208,11 +206,9 @@ async def chat( # Display response if self.verbose: - console.print(f"[green]{self.active_persona.persona_name}:[/green]") - console.print(Panel( - Markdown(response.response), - expand=False - )) + print(f"\n{self.active_persona.persona_name}:") + print(response.response) + print("-" * 40) return response diff --git a/tests/test_integration.py b/tests/test_integration.py index ed0b048..2dbb844 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -17,8 +17,8 @@ async def test_client_health_check(): """Test API health check.""" async with CZeroEngineClient() as client: health = await client.health_check() - assert health["status"] == "healthy" - assert "version" in health + assert health.status == "healthy" + assert health.version @pytest.mark.asyncio @@ -45,6 +45,30 @@ async def test_chat_without_rag(): assert response.model_used +@pytest.mark.asyncio +async def test_chat_with_rag(): + """Test chat with RAG context.""" + async with CZeroEngineClient() as client: + # First ensure we have some documents to search + # This might return empty context if no documents are indexed + response = await client.chat( + message="Tell me about the documents in the system", + use_rag=True, + similarity_threshold=0.3, # Lower threshold to get more results + chunk_limit=5 + ) + assert response.response + assert response.model_used + # context_used might be None if no documents match + if response.context_used: + assert isinstance(response.context_used, list) + if len(response.context_used) > 0: + first_context = response.context_used[0] + assert hasattr(first_context, 'chunk_id') + assert hasattr(first_context, 'content') + assert hasattr(first_context, 'similarity') + + @pytest.mark.asyncio async def test_embedding_generation(): """Test embedding generation.""" @@ -88,7 +112,7 @@ async def test_knowledge_base_workflow(): chunk_size=100 ) - assert result["workspace_id"] + assert result["workspace"]["id"] assert result["files_processed"] >= 2 assert result["chunks_created"] > 0 @@ -199,6 +223,51 @@ async def test_semantic_search(): pass +@pytest.mark.asyncio +async def test_hierarchical_search(): + """Test hierarchical search functionality.""" + async with CZeroEngineClient() as client: + # Test basic search without hierarchy + results = await client.semantic_search( + query="test query", + limit=5, + include_hierarchy=False + ) + assert hasattr(results, 'results') + + # Test search with hierarchy information + results_with_hierarchy = await client.semantic_search( + query="test query", + limit=5, + include_hierarchy=True + ) + assert hasattr(results_with_hierarchy, 'results') + # Check that results have hierarchy fields + if results_with_hierarchy.results: + first_result = results_with_hierarchy.results[0] + assert hasattr(first_result, 'parent_chunk') + assert hasattr(first_result, 'hierarchy_path') + assert hasattr(first_result, 'document_content') + + # Test search at specific hierarchy level (sections) + section_results = await client.semantic_search( + query="test query", + limit=5, + hierarchy_level="0", # Sections only + include_hierarchy=True + ) + assert hasattr(section_results, 'results') + + # Test search at specific hierarchy level (paragraphs) + paragraph_results = await client.semantic_search( + query="test query", + limit=5, + hierarchy_level="1", # Paragraphs only + include_hierarchy=True + ) + assert hasattr(paragraph_results, 'results') + + @pytest.mark.asyncio async def test_error_handling(): """Test error handling.""" @@ -212,12 +281,14 @@ async def test_error_handling(): def test_sync_operations(): - """Test that sync operations raise appropriate errors.""" - client = CZeroEngineClient() + """Test that sync operations work correctly with asyncio.run.""" + async def run_health_check(): + async with CZeroEngineClient() as client: + return await client.health_check() - # Should not be able to use client without async context - with pytest.raises(RuntimeError): - asyncio.run(client.health_check()) + # Should be able to run with asyncio.run + result = asyncio.run(run_health_check()) + assert result.status == "healthy" if __name__ == "__main__":