Diomedes Git
commited on
Commit
·
a79e071
1
Parent(s):
850fc8d
added cursor.md files to root and sub folders, deleted some stuff
Browse files- dev_diary.md +0 -109
- phase2.md +0 -84
- phase3.md +0 -34
- project_analysis.md +0 -55
- src/cluas_mcp/common/api_clients.py +0 -149
- src/cluas_mcp/common/cache.py +0 -20
- src/cluas_mcp/common/cursor.md +0 -2
- steps_taken.md +0 -13
- tests/olderidea.py +0 -395
- ticket_list.md +0 -67
dev_diary.md
DELETED
|
@@ -1,109 +0,0 @@
|
|
| 1 |
-
# Development Diary
|
| 2 |
-
|
| 3 |
-
## 2024-01-XX - MVP Character Skeletons and Gradio Group Chat
|
| 4 |
-
|
| 5 |
-
### Goal
|
| 6 |
-
Create working skeletons for the three missing characters (Magpie, Raven, Crow) with placeholder tools, expand the MCP server to handle all tools, and build a Gradio group chat interface for the hackathon demo.
|
| 7 |
-
|
| 8 |
-
### Implementation
|
| 9 |
-
|
| 10 |
-
**Character Skeletons Created:**
|
| 11 |
-
- **Magpie** (Sanguine temperament): Enthusiastic trend-spotter with tools: `search_web`, `find_trending_topics`, `get_quick_facts`
|
| 12 |
-
- **Raven** (Choleric temperament): Passionate activist with tools: `search_news`, `get_environmental_data`, `verify_claim`
|
| 13 |
-
- **Crow** (Phlegmatic temperament): Calm observer with tools: `get_bird_sightings`, `get_weather_patterns`, `analyze_temporal_patterns`
|
| 14 |
-
|
| 15 |
-
All characters follow the Corvus pattern with Groq client setup, system prompts matching their temperaments, and stub `respond()` methods returning mock messages for MVP.
|
| 16 |
-
|
| 17 |
-
**Tool Entrypoints:**
|
| 18 |
-
Created three new entrypoint modules grouped by tool type:
|
| 19 |
-
- `src/cluas_mcp/web/web_search_entrypoint.py` - 3 functions
|
| 20 |
-
- `src/cluas_mcp/news/news_search_entrypoint.py` - 3 functions
|
| 21 |
-
- `src/cluas_mcp/observation/observation_entrypoint.py` - 3 functions
|
| 22 |
-
|
| 23 |
-
All return structured mock data matching expected real response formats, with TODO comments for future full implementation.
|
| 24 |
-
|
| 25 |
-
**MCP Server Expansion:**
|
| 26 |
-
Updated `src/cluas_mcp/server.py` to:
|
| 27 |
-
- List all 10 tools (9 new + academic_search) in `list_tools()`
|
| 28 |
-
- Route all tool calls to appropriate entrypoints in `call_tool()`
|
| 29 |
-
- Added formatting functions for all tool result types
|
| 30 |
-
|
| 31 |
-
**Gradio Interface:**
|
| 32 |
-
Built `src/gradio/app.py` with:
|
| 33 |
-
- Sequential responses from all 4 characters
|
| 34 |
-
- Character names and emojis displayed
|
| 35 |
-
- Conversation history maintained
|
| 36 |
-
- Simple, MVP-focused implementation (no async handling yet)
|
| 37 |
-
|
| 38 |
-
### Issues Encountered and Resolved
|
| 39 |
-
|
| 40 |
-
1. **Circular Import Issue**: `src/gradio/__init__.py` was causing circular imports. **Resolution**: Deleted the file entirely as it wasn't needed. Updated root `app.py` to import directly from `src.gradio.app`.
|
| 41 |
-
|
| 42 |
-
2. **Import Path Inconsistencies**: Several files had incorrect import paths (missing `src.` prefix):
|
| 43 |
-
- `src/gradio/app.py` - character imports
|
| 44 |
-
- `src/cluas_mcp/academic/semantic_scholar.py`
|
| 45 |
-
- `src/cluas_mcp/academic/pubmed.py`
|
| 46 |
-
- `src/cluas_mcp/academic/arxiv.py`
|
| 47 |
-
- `src/cluas_mcp/academic/thing.py`
|
| 48 |
-
|
| 49 |
-
**Resolution**: Fixed all imports to use consistent `src.` prefix pattern.
|
| 50 |
-
|
| 51 |
-
3. **Gradio API Compatibility**: `theme=gr.themes.Soft()` parameter not supported in this Gradio version. **Resolution**: Removed the theme parameter.
|
| 52 |
-
|
| 53 |
-
4. **Gradio 6.x Migration**: The initial implementation used Gradio 5.x tuple format for chat history. **Resolution**: Migrated to Gradio 6.x messages format with structured content blocks:
|
| 54 |
-
- Changed from `List[Tuple[str, str]]` to `List[dict]` with `{"role": "user/assistant", "content": [{"type": "text", "text": "..."}]}`
|
| 55 |
-
- Updated `get_character_response()` to parse Gradio 6.x format and extract text from content blocks
|
| 56 |
-
- Updated `chat_fn()` to return messages in the new structured format
|
| 57 |
-
- Verified compatibility with Gradio 6.0.0-dev.4
|
| 58 |
-
|
| 59 |
-
5. **Groq Tool Calling Error**: "Tool choice is none, but model called a tool" (400 error). **Resolution**: Added `tool_choice="auto"` parameter to both Corvus and Magpie's Groq API calls. This allows the model to decide whether to use tools, rather than defaulting to None which rejects tool calls.
|
| 60 |
-
|
| 61 |
-
### Testing
|
| 62 |
-
- All characters instantiate successfully
|
| 63 |
-
- Character responses work (stub implementations)
|
| 64 |
-
- Gradio app imports and initializes correctly
|
| 65 |
-
- All imports resolve properly
|
| 66 |
-
- No linter errors
|
| 67 |
-
|
| 68 |
-
### Status
|
| 69 |
-
MVP complete and working. Magpie now has full Groq integration with tool calling. Raven and Crow still have stub implementations. All placeholder tools return structured mock data. Ready for hackathon demo. Future enhancements: full tool implementations for Raven/Crow, async responses, memory functionality, tool usage indicators.
|
| 70 |
-
|
| 71 |
-
### Character Integration Progress
|
| 72 |
-
- ✅ **Corvus**: Full Groq integration with academic_search tool (working)
|
| 73 |
-
- ✅ **Magpie**: Full Groq integration with 3 tools (search_web, find_trending_topics, get_quick_facts) - just implemented
|
| 74 |
-
- ⏳ **Raven**: Stub implementation (needs Groq integration)
|
| 75 |
-
- ⏳ **Crow**: Stub implementation (needs Groq integration)
|
| 76 |
-
|
| 77 |
-
### Commits
|
| 78 |
-
- `71f5dac` - Added character skeletons (Magpie, Raven, Crow) with placeholder tools, MCP server routes, and Gradio group chat interface
|
| 79 |
-
- `1868ae1` - Fixed import paths: removed gradio __init__.py, fixed all src. imports, removed theme parameter
|
| 80 |
-
- `1f44947` - Added documentation: steps_taken.md and dev_diary.md for character skeletons implementation
|
| 81 |
-
- `8696667` - Migrated chat_fn to Gradio 6.x messages format with structured content blocks
|
| 82 |
-
- `28718e5` - Implemented full Groq integration for Magpie with tool calling for search_web, find_trending_topics, and get_quick_facts
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
__________
|
| 86 |
-
End-of-Day Progress Report (2025-11-21, 23:26)
|
| 87 |
-
Accomplished
|
| 88 |
-
Feature branch merged
|
| 89 |
-
Merged stable feature branch (character skeletons, MCP server tools, Gradio 6.x compatibility) into main; successfully pulled remote updates.
|
| 90 |
-
Groq integration for Magpie
|
| 91 |
-
Implemented complete Groq integration for Magpie, including tool-calling logic for all tools: search_web, find_trending_topics, get_quick_facts.
|
| 92 |
-
Added helper formatting and async support for responses.
|
| 93 |
-
Documented individual steps and committed granularly.
|
| 94 |
-
Fixed Groq API tool_use error
|
| 95 |
-
Resolved 400 error ("Tool choice is none, but model called a tool") by adding tool_choice="auto" for both Corvus and Magpie. Now both characters can call their tools from the LLM.
|
| 96 |
-
Documentation
|
| 97 |
-
Updated dev diary and steps_taken.md for each significant change.
|
| 98 |
-
Maintained clean commit history (granular, logical commits).
|
| 99 |
-
Current Status
|
| 100 |
-
Corvus & Magpie: Working Groq integration & tool calls.
|
| 101 |
-
MCP: All 10 tools listed, tool MPC routing correct.
|
| 102 |
-
Gradio Chat: All 4 characters visible, system runs.
|
| 103 |
-
Raven & Crow: Still stubs (to be upgraded).
|
| 104 |
-
No uncommitted changes; project state is stable, demo-ready for MVP.
|
| 105 |
-
Next Steps
|
| 106 |
-
Bring Raven and Crow up to parity (Groq+tools).
|
| 107 |
-
Continue UI/polish and E2E testing.
|
| 108 |
-
Prepare for next rounds of hackathon demo/test/iteration.
|
| 109 |
-
________________
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
phase2.md
DELETED
|
@@ -1,84 +0,0 @@
|
|
| 1 |
-
# Phase 2: Hackathon Preparation Plan
|
| 2 |
-
|
| 3 |
-
This document outlines a practical, 6-day plan to take the "Cluas" project from its current state to a complete, polished, and shareable project for the hackathon. The focus is on stability, user experience, and clear presentation.
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
|
| 7 |
-
## **Key Areas of Focus**
|
| 8 |
-
|
| 9 |
-
1. **User Experience (UX) & Interface Polish:** First impressions are critical. A clean, intuitive, and responsive UI will make the project stand out.
|
| 10 |
-
2. **Core Functionality Hardening:** The main features must be robust and handle errors gracefully.
|
| 11 |
-
3. **Deployment & Accessibility:** Judges need to be able to access and use the project with minimal friction.
|
| 12 |
-
4. **Presentation & Documentation:** A compelling narrative and clear instructions are as important as the code itself.
|
| 13 |
-
|
| 14 |
-
---
|
| 15 |
-
|
| 16 |
-
## **6-Day Action Plan**
|
| 17 |
-
|
| 18 |
-
### **Days 1-2: UI/UX & Core Logic Refinement**
|
| 19 |
-
|
| 20 |
-
The goal for these two days is to enhance the front-end experience and make the backend more resilient.
|
| 21 |
-
|
| 22 |
-
* **UI Polish:**
|
| 23 |
-
* **Task:** Review the Gradio interface for clarity. Can a new user understand it immediately?
|
| 24 |
-
* **Task:** Implement loading indicators for long-running processes (e.g., when the AI council is "thinking"). This provides crucial feedback to the user.
|
| 25 |
-
* **Task:** Add a clear "About" or "How to Use" section within the Gradio app. Explain the roles of the different corvids.
|
| 26 |
-
* **Task:** Improve the visual separation of messages from different agents. Use icons, labels, or colors to indicate who is speaking.
|
| 27 |
-
|
| 28 |
-
* **Error Handling:**
|
| 29 |
-
* **Task:** Implement graceful error handling for external API failures (e.g., Groq, academic search tools). The app should display a user-friendly message like "Sorry, I couldn't fetch that information. Please try again." instead of crashing.
|
| 30 |
-
* **Task:** Add basic input validation to prevent trivial errors.
|
| 31 |
-
|
| 32 |
-
* **Conversation Flow:**
|
| 33 |
-
* **Task:** Review the final synthesized answer. Ensure it's well-formatted and clearly presented as the culmination of the council's discussion.
|
| 34 |
-
|
| 35 |
-
### **Days 3-4: Deployment & Testing**
|
| 36 |
-
|
| 37 |
-
The focus now shifts to making the project accessible and finding any remaining bugs.
|
| 38 |
-
|
| 39 |
-
* **Deployment:**
|
| 40 |
-
* **Task:** Choose a deployment platform. **Hugging Face Spaces** is an excellent and free choice for Gradio applications.
|
| 41 |
-
* **Task:** Verify that `pyproject.toml` and `requirements.txt` contain all necessary dependencies for a clean installation.
|
| 42 |
-
* **Task:** Create an `.env.example` file to show what environment variables are needed (like `GROQ_API_KEY`), but without a real key.
|
| 43 |
-
* **Task:** Write clear, step-by-step deployment instructions in the `README.md`.
|
| 44 |
-
* **Task:** Deploy a live version of the app and test it thoroughly.
|
| 45 |
-
|
| 46 |
-
* **End-to-End Testing:**
|
| 47 |
-
* **Task:** Manually run through 5-10 complex user queries. Try to break the application.
|
| 48 |
-
* **Task:** Ask a friend or colleague (ideally non-technical) to use the app. Watch how they interact with it and gather feedback. Fresh eyes will find issues you've become blind to.
|
| 49 |
-
* **Task:** Fix any critical bugs discovered during this testing phase.
|
| 50 |
-
|
| 51 |
-
### **Day 5: Documentation & Presentation**
|
| 52 |
-
|
| 53 |
-
With a stable, deployed app, the focus is on crafting the project's story.
|
| 54 |
-
|
| 55 |
-
* **README.md Overhaul:**
|
| 56 |
-
* **Task:** Update the `README.md` to be a comprehensive guide for a hackathon judge. It should be the central hub of your project.
|
| 57 |
-
* **Task:** Add a compelling one-paragraph project pitch at the top. What is Cluas, and why is it cool?
|
| 58 |
-
* **Task:** **Create and embed a short demo video or GIF** showing the app in action. This is the single most effective way to communicate your project's value.
|
| 59 |
-
* **Task:** Add a clear "Getting Started" section for running the project locally.
|
| 60 |
-
* **Task:** Include a prominent link to the live demo you deployed on Day 4.
|
| 61 |
-
* **Task:** Add a brief "Technology Stack" section listing the key frameworks and APIs used.
|
| 62 |
-
|
| 63 |
-
* **Prepare Presentation Materials:**
|
| 64 |
-
* **Task:** Create a short slide deck (5-7 slides) or a 2-minute video script explaining the project.
|
| 65 |
-
* **Task:** Focus on the **Problem**, the **Solution (Your App)**, and the **Technology**.
|
| 66 |
-
* **Task:** Practice your pitch. Be ready to explain your project clearly and concisely.
|
| 67 |
-
|
| 68 |
-
### **Day 6: Final Polish & Submission**
|
| 69 |
-
|
| 70 |
-
This is the last mile. No new features, just refinement.
|
| 71 |
-
|
| 72 |
-
* **Final Code Freeze:**
|
| 73 |
-
* **Task:** Stop adding new features. Only commit critical, show-stopping bug fixes.
|
| 74 |
-
* **Task:** Clean up the codebase: remove commented-out code, add docstrings to key functions, and ensure consistent formatting.
|
| 75 |
-
|
| 76 |
-
* **Review Submission Requirements:**
|
| 77 |
-
* **Task:** Double-check all the hackathon rules and submission requirements. Don't be disqualified on a technicality.
|
| 78 |
-
|
| 79 |
-
* **Final Polish:**
|
| 80 |
-
* **Task:** Do one last end-to-end test of the live demo.
|
| 81 |
-
* **Task:** Proofread all your documentation (`README.md`, presentation).
|
| 82 |
-
|
| 83 |
-
* **Submit!**
|
| 84 |
-
* **Task:** Submit your project with confidence. Good luck!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
phase3.md
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
# Phase 3 Analysis of the Cluas Project
|
| 2 |
-
|
| 3 |
-
This document provides an analysis of the Cluas project's current state. The analysis is based on a review of the project's documentation, source code, and tests.
|
| 4 |
-
|
| 5 |
-
## Overall Impressions
|
| 6 |
-
|
| 7 |
-
The Cluas project is in a very strong state. It has a clear and compelling vision, a well-designed modular architecture, and a solid implementation of its core features. The project is well on its way to achieving its goal of creating a "dialectic research tool" where AI agents collaborate to answer user questions.
|
| 8 |
-
|
| 9 |
-
## Strengths
|
| 10 |
-
|
| 11 |
-
* **Strong Concept and Vision:** The project's goal of creating a multi-agent AI research tool with memory and collaborative capabilities is both ambitious and well-defined. The `README.md` and `GEMINI.md` files do an excellent job of articulating this vision.
|
| 12 |
-
* **Excellent Modular Architecture:** The codebase is well-organized and easy to understand. The separation of concerns between the UI (`src/gradio`), the AI characters (`src/characters`), and the tools (`src/cluas_mcp`) is a key strength. This modularity will make it much easier to maintain and extend the project in the future.
|
| 13 |
-
* **Well-Defined Characters:** The four AI characters—Corvus, Magpie, Raven, and Crow—are well-defined with distinct personalities, roles, and tools. The use of detailed system prompts to shape the characters' behavior is very effective.
|
| 14 |
-
* **Memory Implementation:** The memory system, particularly as implemented for Corvus, is a standout feature. The ability for a character to recall past conversations and learned information is crucial to the project's vision of "research that remembers."
|
| 15 |
-
* **Robust Tool Integration:** The system for allowing characters to use external tools is well-designed. The code for handling tool calls, parsing arguments, and incorporating tool outputs into the conversation is robust and effective. The modular design of the `cluas_mcp` makes it easy to add new data sources and capabilities.
|
| 16 |
-
* **Flexible LLM Backend:** The support for both the Groq API and a local Ollama instance provides valuable flexibility for development and deployment.
|
| 17 |
-
* **Solid Testing Strategy:** The project includes a suite of tests, including integration tests that make live API calls and a structure for mocked, non-calling tests. This commitment to testing is essential for ensuring the quality and reliability of the codebase.
|
| 18 |
-
|
| 19 |
-
## Areas for Improvement and Next Steps
|
| 20 |
-
|
| 21 |
-
The project has a strong foundation, but there are several areas where it could be improved and extended.
|
| 22 |
-
|
| 23 |
-
* **Complete Character Implementations:**
|
| 24 |
-
* **Crow:** Based on the current review, it's likely that Crow's implementation is not as complete as Corvus's and Magpie's. Flesh out Crow's personality, tools (e.g., for nature observation, pattern analysis), and response logic.
|
| 25 |
-
* **Raven:** Similarly, ensure Raven's implementation as a "news monitor and fact-checker" is fully realized.
|
| 26 |
-
* **Full Ollama Support:** The Ollama backend is not yet fully implemented for all characters (e.g., Magpie). Completing this would provide a robust and fully-functional local development environment, which is a significant advantage.
|
| 27 |
-
* **Enhanced UI Error Handling:** While the backend has some error handling, this could be more effectively communicated to the user in the Gradio interface. For example, if a tool fails, the UI could display a clear, user-friendly message explaining what went wrong, rather than just having the character fall silent or give a generic error message.
|
| 28 |
-
* **Reduce Code Duplication:** There is some repetition in the `_respond_groq` methods of the different character classes. Consider creating a base `Character` class that abstracts some of the common logic for handling LLM responses and tool calls. This would reduce code duplication and make the character classes easier to maintain.
|
| 29 |
-
* **Structured Configuration Management:** Currently, API keys and other configuration are loaded directly from a `.env` file. As the project grows, it would be beneficial to adopt a more structured approach to configuration management. Libraries like Pydantic's settings management can provide type-safe, validated configuration objects, which can help to prevent configuration-related errors.
|
| 30 |
-
* **More Sophisticated Agent Interaction:** The current interaction model is sequential, with each character responding in a fixed order. To fully realize the "dialectic" vision, consider implementing a more dynamic interaction model. For example, a character could choose to respond to another character's statement, or a "moderator" agent could guide the conversation.
|
| 31 |
-
|
| 32 |
-
## Conclusion
|
| 33 |
-
|
| 34 |
-
The Cluas project is an impressive piece of work. It is a well-designed and well-implemented multi-agent AI system with a clear and compelling vision. The project's strengths far outweigh its current limitations, and it has a strong foundation for future development. By addressing the areas for improvement outlined above, the project can move even closer to its goal of creating a truly innovative and powerful research tool.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
project_analysis.md
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
# Project Analysis: Cluas
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
|
| 5 |
-
**Cluas** is a Python-based multi-agent AI research tool designed for dialectic deliberation. The name "Cluas" is Gaelic for "ear," reflecting its purpose as a listening and information-gathering system. The project uses a "Corvid Council" of four AI agents (Corvus, Magpie, Raven, and Crow), each with a distinct persona and a specialized set of tools for research. The system facilitates a conversational research process where these agents can collaborate, debate, and build upon shared knowledge over time.
|
| 6 |
-
|
| 7 |
-
The primary interface is a web application built with Gradio, allowing users to interact with the council in two modes: a "Collaborative Mode" for synthesized answers and an "Active Mode" for direct participation in the discussion.
|
| 8 |
-
|
| 9 |
-
## Key Components
|
| 10 |
-
|
| 11 |
-
### 1. Application Core
|
| 12 |
-
|
| 13 |
-
- **`app.py`**: The main entry point for the Gradio web application.
|
| 14 |
-
- **`src/gradio/app.py`**: Contains the UI and logic for the Gradio chat interface. It manages the interaction between the user and the AI characters.
|
| 15 |
-
- **`src/orchestrator.py`**: This file is intended to be the central coordinator for the AI agents' interactions, managing the dialectic process, and handling shared memory. It is currently a placeholder, with the Gradio app handling basic orchestration.
|
| 16 |
-
- **`src/characters/`**: This directory defines the different AI agent personas:
|
| 17 |
-
- `corvus.py`: The scholar, focused on academic research.
|
| 18 |
-
- `magpie.py`: The enthusiast, skilled at finding trends and quick facts.
|
| 19 |
-
- `raven.py`: The activist, focused on news and fact-checking.
|
| 20 |
-
- `crow.py`: The observer, specializing in environmental and temporal patterns.
|
| 21 |
-
|
| 22 |
-
### 2. Tooling and Integrations (MCP - Multi-Component Platform)
|
| 23 |
-
|
| 24 |
-
- **`src/cluas_mcp/server.py`**: An MCP server that exposes the various research tools to the AI agents. This allows the agents to perform actions like searching academic papers, news, and the web.
|
| 25 |
-
- **`src/cluas_mcp/`**: This directory is organized by domain, with entry points for different types of searches:
|
| 26 |
-
- **`academic/`**: Integrates with ArXiv, PubMed, and Semantic Scholar.
|
| 27 |
-
- **`news/`**: Provides news search and claim verification.
|
| 28 |
-
- **`web/`**: For general web searches and trending topics.
|
| 29 |
-
- **`observation/`**: Connects to eBird for bird sighting data.
|
| 30 |
-
- **`src/cluas_mcp/common/`**: Contains shared utilities for API clients, caching, and data formatting.
|
| 31 |
-
|
| 32 |
-
### 3. Dependencies
|
| 33 |
-
|
| 34 |
-
- **`pyproject.toml` & `requirements.txt`**: Define the project dependencies. Key libraries include:
|
| 35 |
-
- `gradio`: For the web UI.
|
| 36 |
-
- `fastmcp` & `mcp`: For the multi-agent communication and tool-serving framework.
|
| 37 |
-
- `groq`: Likely for interacting with a large language model API.
|
| 38 |
-
- `feedparser`, `requests`, `tenacity`: For fetching data from external APIs and web sources.
|
| 39 |
-
- `pytest`: For testing.
|
| 40 |
-
|
| 41 |
-
### 4. Testing
|
| 42 |
-
|
| 43 |
-
- **`tests/`**: The project has a testing suite with `pytest`.
|
| 44 |
-
- **`clients/`**: Contains tests for the various API clients (ArXiv, PubMed, etc.), with both live and mocked tests.
|
| 45 |
-
- **`integration/`**: Includes integration tests for the search entry points.
|
| 46 |
-
|
| 47 |
-
## Analysis Summary
|
| 48 |
-
|
| 49 |
-
The project is well-structured, with a clear separation of concerns between the UI (Gradio), the agent personas (characters), and the tool implementations (MCP). The use of a multi-agent system is a sophisticated approach to research, allowing for a more robust and nuanced exploration of topics.
|
| 50 |
-
|
| 51 |
-
The `orchestrator.py` file indicates a plan for a more advanced system that can manage complex interactions and a persistent shared memory, which is the core of the "dialectic" process described in the README.
|
| 52 |
-
|
| 53 |
-
The file `src/cluas_mcp/academic/thing.py` appears to be a temporary or test file and should be reviewed.
|
| 54 |
-
|
| 55 |
-
Overall, Cluas is an ambitious and interesting project with a solid foundation. The immediate next steps would likely involve implementing the `orchestrator.py` to realize the full vision of a dialectic research tool.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/cluas_mcp/common/api_clients.py
DELETED
|
@@ -1,149 +0,0 @@
|
|
| 1 |
-
from common.http import fetch_with_retry
|
| 2 |
-
import requests
|
| 3 |
-
import feedparser
|
| 4 |
-
import xml.etree.ElementTree as ET
|
| 5 |
-
import urllib.parse
|
| 6 |
-
from typing import List, Optional
|
| 7 |
-
|
| 8 |
-
class PubMedClient:
|
| 9 |
-
|
| 10 |
-
@staticmethod
|
| 11 |
-
def parse_id_list(xml: str) -> List[str]:
|
| 12 |
-
"""Parse XML and return a list of PubMed IDs."""
|
| 13 |
-
try:
|
| 14 |
-
root = ET.fromstring(xml)
|
| 15 |
-
except ET.ParseError:
|
| 16 |
-
return [] # invalid XML or rate limit page
|
| 17 |
-
|
| 18 |
-
id_list = root.find(".//IdList")
|
| 19 |
-
if id_list is None:
|
| 20 |
-
return []
|
| 21 |
-
|
| 22 |
-
return [elem.text for elem in id_list.findall("Id") if elem.text]
|
| 23 |
-
|
| 24 |
-
@staticmethod
|
| 25 |
-
def pubmed_search(
|
| 26 |
-
keywords: List[str],
|
| 27 |
-
extra_terms: Optional[List[str]] = None,
|
| 28 |
-
retmax: int = 20,
|
| 29 |
-
) -> List[str]:
|
| 30 |
-
"""
|
| 31 |
-
Search PubMed for (keywords OR ...) AND (extra_terms OR ...).
|
| 32 |
-
Returns PubMed IDs.
|
| 33 |
-
"""
|
| 34 |
-
# building grouped OR clauses
|
| 35 |
-
base = "(" + " OR ".join(keywords) + ")"
|
| 36 |
-
if extra_terms:
|
| 37 |
-
base = f"{base} AND ({' OR '.join(extra_terms)})"
|
| 38 |
-
|
| 39 |
-
# URL-encode the full term string
|
| 40 |
-
term = urllib.parse.quote(base)
|
| 41 |
-
|
| 42 |
-
url = (
|
| 43 |
-
"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
|
| 44 |
-
f"?db=pubmed&term={term}&retmax={retmax}&retmode=xml"
|
| 45 |
-
)
|
| 46 |
-
|
| 47 |
-
try:
|
| 48 |
-
response = fetch_with_retry(url)
|
| 49 |
-
response.raise_for_status()
|
| 50 |
-
return PubMedClient.parse_id_list(response.text)
|
| 51 |
-
|
| 52 |
-
except requests.exceptions.RequestException:
|
| 53 |
-
# log instead of print, lol
|
| 54 |
-
return []
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
class SemanticScholarClient:
|
| 62 |
-
def search(self, query, max_results=5):
|
| 63 |
-
# placeholder: implement Semantic Scholar API call
|
| 64 |
-
return []
|
| 65 |
-
|
| 66 |
-
class ArxivClient:
|
| 67 |
-
KEYWORDS = ['corvid','crow','raven','corvus','jay','magpie','jackdaw','rook','chough','nutcracker']
|
| 68 |
-
def search(self, query, max_results=5):
|
| 69 |
-
q = " OR ".join([query] + self.KEYWORDS)
|
| 70 |
-
url = (
|
| 71 |
-
f"https://export.arxiv.org/api/query?"
|
| 72 |
-
f"search_query=all:({q})&start=0&max_results={max_results}&"
|
| 73 |
-
"sortBy=lastUpdatedDate&sortOrder=descending"
|
| 74 |
-
)
|
| 75 |
-
data = requests.get(url).text
|
| 76 |
-
feed = feedparser.parse(data)
|
| 77 |
-
results = []
|
| 78 |
-
for entry in feed.entries:
|
| 79 |
-
if not getattr(entry, "summary", "").strip():
|
| 80 |
-
continue
|
| 81 |
-
results.append({
|
| 82 |
-
"title": getattr(entry, "title", "Untitled"),
|
| 83 |
-
"abstract": getattr(entry, "summary", ""),
|
| 84 |
-
"authors": [a.name for a in getattr(entry, "authors", [])],
|
| 85 |
-
"published": getattr(entry, "published", ""),
|
| 86 |
-
"arxiv_link": getattr(entry, "link", "")
|
| 87 |
-
})
|
| 88 |
-
return results
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
# possible endpoint?
|
| 94 |
-
# https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&retmode=json&term=corvid+AND+memory
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
# from Bio import Entrez
|
| 100 |
-
# Entrez.email = "your.email@domain.tld" # required by NCBI
|
| 101 |
-
# Entrez.api_key = "YOUR_KEY_IF_YOU_HAVE_ONE"
|
| 102 |
-
|
| 103 |
-
# KEYWORDS = ['corvid','crow','raven','corvus','jay','magpie','jackdaw','rook','chough','nutcracker']
|
| 104 |
-
# SECONDARY = ['memory', 'feeding']
|
| 105 |
-
|
| 106 |
-
# term = "(" + " OR ".join(KEYWORDS) + ")" + " AND (" + " OR ".join(SECONDARY) + ")"
|
| 107 |
-
# handle = Entrez.esearch(db="pubmed", term=term, retmax=100) # adjust retmax
|
| 108 |
-
# record = Entrez.read(handle)
|
| 109 |
-
# ids = record["IdList"]
|
| 110 |
-
|
| 111 |
-
# for pmid in ids:
|
| 112 |
-
# handle2 = Entrez.efetch(db="pubmed", id=pmid, retmode="xml")
|
| 113 |
-
# rec = Entrez.read(handle2)
|
| 114 |
-
# article = rec['PubmedArticle'][0]
|
| 115 |
-
# # parse title
|
| 116 |
-
# title = article['MedlineCitation']['Article']['ArticleTitle']
|
| 117 |
-
# # parse authors
|
| 118 |
-
# authors = article['MedlineCitation']['Article']['AuthorList']
|
| 119 |
-
# first_author = authors[0]['LastName'] + ", " + authors[0]['ForeName']
|
| 120 |
-
# author_str = first_author + (", et al" if len(authors) > 1 else "")
|
| 121 |
-
# # parse abstract
|
| 122 |
-
# abstract = ""
|
| 123 |
-
# if 'Abstract' in article['MedlineCitation']['Article']:
|
| 124 |
-
# abstract = " ".join([x for x in article['MedlineCitation']['Article']['Abstract']['AbstractText']])
|
| 125 |
-
# # parse DOI
|
| 126 |
-
# doi = None
|
| 127 |
-
# for aid in article['PubmedData']['ArticleIdList']:
|
| 128 |
-
# if aid.attributes['IdType'] == 'doi':
|
| 129 |
-
# doi = str(aid)
|
| 130 |
-
# # parse a “conclusion” if structured abstract includes it
|
| 131 |
-
# conclusion = None
|
| 132 |
-
# # one simple heuristic: look for segments labeled 'CONCLUSION' in structured abstract
|
| 133 |
-
# if 'Abstract' in article['MedlineCitation']['Article']:
|
| 134 |
-
# for sec in article['MedlineCitation']['Article']['Abstract']['AbstractText']:
|
| 135 |
-
# if hasattr(sec, "attributes") and sec.attributes.get('Label', '').upper() == 'CONCLUSION':
|
| 136 |
-
# conclusion = str(sec)
|
| 137 |
-
# # fallback: maybe take the last sentence of abstract
|
| 138 |
-
# if conclusion is None and abstract:
|
| 139 |
-
# conclusion = abstract.split('.')[-2] + '.'
|
| 140 |
-
|
| 141 |
-
# # now you have doi, title, author_str, abstract, conclusion
|
| 142 |
-
# print(pmid, doi, title, author_str, conclusion)
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
# f'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term={query}'
|
| 149 |
-
# f'https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id={id[0]}&retmode=xml&rettype=abstract'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/cluas_mcp/common/cache.py
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
|
| 4 |
-
class CacheManager:
|
| 5 |
-
def __init__(self, cache_file: str):
|
| 6 |
-
self.cache_file = Path(cache_file)
|
| 7 |
-
if not self.cache_file.exists():
|
| 8 |
-
self.cache_file.write_text(json.dumps({}))
|
| 9 |
-
|
| 10 |
-
def get(self, query: str):
|
| 11 |
-
with open(self.cache_file, "r") as f:
|
| 12 |
-
data = json.load(f)
|
| 13 |
-
return data.get(query)
|
| 14 |
-
|
| 15 |
-
def set(self, query: str, results):
|
| 16 |
-
with open(self.cache_file, "r") as f:
|
| 17 |
-
data = json.load(f)
|
| 18 |
-
data[query] = results
|
| 19 |
-
with open(self.cache_file, "w") as f:
|
| 20 |
-
json.dump(data, f, indent=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/cluas_mcp/common/cursor.md
CHANGED
|
@@ -7,8 +7,6 @@ Shared helpers: http, cache, memory, formatting.
|
|
| 7 |
- Preserve helper interfaces.
|
| 8 |
|
| 9 |
# Important files
|
| 10 |
-
- api_clients.py
|
| 11 |
-
- cache.py
|
| 12 |
- formatting.py
|
| 13 |
- http.py
|
| 14 |
- memory.py
|
|
|
|
| 7 |
- Preserve helper interfaces.
|
| 8 |
|
| 9 |
# Important files
|
|
|
|
|
|
|
| 10 |
- formatting.py
|
| 11 |
- http.py
|
| 12 |
- memory.py
|
steps_taken.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
# Steps Taken
|
| 2 |
-
|
| 3 |
-
## 2024-01-XX - Character Skeletons and Gradio Chat Implementation
|
| 4 |
-
|
| 5 |
-
1. Created character skeletons for Magpie, Raven, and Crow following Corvus pattern
|
| 6 |
-
2. Created tool entrypoint stubs grouped by type (web, news, observation) with structured mock data
|
| 7 |
-
3. Updated MCP server to route all 9 new tools plus existing academic_search
|
| 8 |
-
4. Built Gradio group chat interface with sequential character responses
|
| 9 |
-
5. Fixed import paths: removed gradio __init__.py, fixed all src. imports, removed unsupported theme parameter
|
| 10 |
-
6. Tested and verified all characters instantiate and respond correctly
|
| 11 |
-
7. Migrated chat_fn to Gradio 6.x messages format with structured content blocks (per Gradio 6 migration guide)
|
| 12 |
-
8. Implemented full Groq integration for Magpie with tool calling (search_web, find_trending_topics, get_quick_facts)
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/olderidea.py
DELETED
|
@@ -1,395 +0,0 @@
|
|
| 1 |
-
import requests
|
| 2 |
-
import feedparser
|
| 3 |
-
import xml.etree.ElementTree as ET
|
| 4 |
-
import urllib.parse
|
| 5 |
-
from typing import List, Optional, Dict, Any
|
| 6 |
-
import logging
|
| 7 |
-
from http import fetch_with_retry
|
| 8 |
-
|
| 9 |
-
logger = logging.getLogger(__name__)
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
class PubMedClient:
|
| 13 |
-
"""Client for searching and fetching articles from PubMed."""
|
| 14 |
-
|
| 15 |
-
BASE_SEARCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
|
| 16 |
-
BASE_FETCH_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi"
|
| 17 |
-
|
| 18 |
-
@staticmethod
|
| 19 |
-
def parse_id_list(xml: str) -> List[str]:
|
| 20 |
-
"""Parse XML and return a list of PubMed IDs."""
|
| 21 |
-
try:
|
| 22 |
-
root = ET.fromstring(xml)
|
| 23 |
-
except ET.ParseError as e:
|
| 24 |
-
logger.error(f"Failed to parse ID list XML: {e}")
|
| 25 |
-
return []
|
| 26 |
-
|
| 27 |
-
id_list = root.find(".//IdList")
|
| 28 |
-
if id_list is None:
|
| 29 |
-
return []
|
| 30 |
-
|
| 31 |
-
return [elem.text for elem in id_list.findall("Id") if elem.text]
|
| 32 |
-
|
| 33 |
-
@staticmethod
|
| 34 |
-
def parse_articles(xml: str) -> List[Dict[str, Any]]:
|
| 35 |
-
"""Parse PubMed article XML into structured data."""
|
| 36 |
-
try:
|
| 37 |
-
root = ET.fromstring(xml)
|
| 38 |
-
except ET.ParseError as e:
|
| 39 |
-
logger.error(f"Failed to parse articles XML: {e}")
|
| 40 |
-
return []
|
| 41 |
-
|
| 42 |
-
articles = []
|
| 43 |
-
for article_elem in root.findall(".//PubmedArticle"):
|
| 44 |
-
try:
|
| 45 |
-
article = PubMedClient._parse_single_article(article_elem)
|
| 46 |
-
if article:
|
| 47 |
-
articles.append(article)
|
| 48 |
-
except Exception as e:
|
| 49 |
-
logger.warning(f"Failed to parse article: {e}")
|
| 50 |
-
continue
|
| 51 |
-
|
| 52 |
-
return articles
|
| 53 |
-
|
| 54 |
-
@staticmethod
|
| 55 |
-
def _parse_single_article(article_elem: ET.Element) -> Optional[Dict[str, Any]]:
|
| 56 |
-
"""Parse a single PubMed article element."""
|
| 57 |
-
medline = article_elem.find(".//MedlineCitation")
|
| 58 |
-
if medline is None:
|
| 59 |
-
return None
|
| 60 |
-
|
| 61 |
-
article_data = medline.find(".//Article")
|
| 62 |
-
if article_data is None:
|
| 63 |
-
return None
|
| 64 |
-
|
| 65 |
-
# extract PMID
|
| 66 |
-
pmid_elem = medline.find(".//PMID")
|
| 67 |
-
pmid = pmid_elem.text if pmid_elem is not None else None
|
| 68 |
-
|
| 69 |
-
# extract title
|
| 70 |
-
title_elem = article_data.find(".//ArticleTitle")
|
| 71 |
-
title = title_elem.text if title_elem is not None else "Untitled"
|
| 72 |
-
|
| 73 |
-
# extract authors
|
| 74 |
-
authors = []
|
| 75 |
-
author_list = article_data.find(".//AuthorList")
|
| 76 |
-
if author_list is not None:
|
| 77 |
-
for author in author_list.findall(".//Author"):
|
| 78 |
-
last_name = author.find(".//LastName")
|
| 79 |
-
fore_name = author.find(".//ForeName")
|
| 80 |
-
if last_name is not None:
|
| 81 |
-
name = last_name.text
|
| 82 |
-
if fore_name is not None:
|
| 83 |
-
name = f"{last_name.text}, {fore_name.text}"
|
| 84 |
-
authors.append(name)
|
| 85 |
-
|
| 86 |
-
author_str = authors[0] if authors else "Unknown"
|
| 87 |
-
if len(authors) > 1:
|
| 88 |
-
author_str += " et al."
|
| 89 |
-
|
| 90 |
-
# extract abstract
|
| 91 |
-
abstract_parts = []
|
| 92 |
-
abstract_elem = article_data.find(".//Abstract")
|
| 93 |
-
if abstract_elem is not None:
|
| 94 |
-
for abstract_text in abstract_elem.findall(".//AbstractText"):
|
| 95 |
-
if abstract_text.text:
|
| 96 |
-
abstract_parts.append(abstract_text.text)
|
| 97 |
-
abstract = " ".join(abstract_parts)
|
| 98 |
-
|
| 99 |
-
# extract conclusion (from structured abstract)
|
| 100 |
-
conclusion = None
|
| 101 |
-
if abstract_elem is not None:
|
| 102 |
-
for abstract_text in abstract_elem.findall(".//AbstractText"):
|
| 103 |
-
label = abstract_text.get("Label", "")
|
| 104 |
-
if label.upper() in ["CONCLUSION", "CONCLUSIONS"]:
|
| 105 |
-
conclusion = abstract_text.text
|
| 106 |
-
break
|
| 107 |
-
|
| 108 |
-
# fallback: use last sentence of abstract as conclusion
|
| 109 |
-
if conclusion is None and abstract:
|
| 110 |
-
sentences = abstract.split('. ')
|
| 111 |
-
if len(sentences) > 1:
|
| 112 |
-
conclusion = sentences[-2] + '.'
|
| 113 |
-
|
| 114 |
-
# extract DOI
|
| 115 |
-
doi = None
|
| 116 |
-
pubmed_data = article_elem.find(".//PubmedData")
|
| 117 |
-
if pubmed_data is not None:
|
| 118 |
-
article_id_list = pubmed_data.find(".//ArticleIdList")
|
| 119 |
-
if article_id_list is not None:
|
| 120 |
-
for article_id in article_id_list.findall(".//ArticleId"):
|
| 121 |
-
if article_id.get("IdType") == "doi":
|
| 122 |
-
doi = article_id.text
|
| 123 |
-
break
|
| 124 |
-
|
| 125 |
-
# extract publication date
|
| 126 |
-
pub_date = None
|
| 127 |
-
pub_date_elem = article_data.find(".//ArticleDate")
|
| 128 |
-
if pub_date_elem is None:
|
| 129 |
-
pub_date_elem = medline.find(".//PubDate")
|
| 130 |
-
|
| 131 |
-
if pub_date_elem is not None:
|
| 132 |
-
year = pub_date_elem.find(".//Year")
|
| 133 |
-
month = pub_date_elem.find(".//Month")
|
| 134 |
-
day = pub_date_elem.find(".//Day")
|
| 135 |
-
|
| 136 |
-
date_parts = []
|
| 137 |
-
if year is not None:
|
| 138 |
-
date_parts.append(year.text)
|
| 139 |
-
if month is not None:
|
| 140 |
-
date_parts.append(month.text)
|
| 141 |
-
if day is not None:
|
| 142 |
-
date_parts.append(day.text)
|
| 143 |
-
pub_date = "-".join(date_parts)
|
| 144 |
-
|
| 145 |
-
return {
|
| 146 |
-
"pmid": pmid,
|
| 147 |
-
"title": title,
|
| 148 |
-
"authors": authors,
|
| 149 |
-
"author_str": author_str,
|
| 150 |
-
"abstract": abstract,
|
| 151 |
-
"conclusion": conclusion,
|
| 152 |
-
"doi": doi,
|
| 153 |
-
"published": pub_date,
|
| 154 |
-
"pubmed_link": f"https://pubmed.ncbi.nlm.nih.gov/{pmid}/" if pmid else None
|
| 155 |
-
}
|
| 156 |
-
|
| 157 |
-
@staticmethod
|
| 158 |
-
def pubmed_search(
|
| 159 |
-
keywords: List[str],
|
| 160 |
-
extra_terms: Optional[List[str]] = None,
|
| 161 |
-
retmax: int = 20,
|
| 162 |
-
) -> List[str]:
|
| 163 |
-
"""
|
| 164 |
-
Search PubMed for (keywords OR ...) AND (extra_terms OR ...).
|
| 165 |
-
Returns PubMed IDs.
|
| 166 |
-
"""
|
| 167 |
-
# building grouped OR clauses
|
| 168 |
-
base = "(" + " OR ".join(keywords) + ")"
|
| 169 |
-
if extra_terms:
|
| 170 |
-
base = f"{base} AND ({' OR '.join(extra_terms)})"
|
| 171 |
-
|
| 172 |
-
# URL-encode the full term string
|
| 173 |
-
term = urllib.parse.quote(base)
|
| 174 |
-
|
| 175 |
-
url = (
|
| 176 |
-
f"{PubMedClient.BASE_SEARCH_URL}"
|
| 177 |
-
f"?db=pubmed&term={term}&retmax={retmax}&retmode=xml"
|
| 178 |
-
)
|
| 179 |
-
|
| 180 |
-
try:
|
| 181 |
-
response = fetch_with_retry(url)
|
| 182 |
-
response.raise_for_status()
|
| 183 |
-
return PubMedClient.parse_id_list(response.text)
|
| 184 |
-
except requests.exceptions.RequestException as e:
|
| 185 |
-
logger.error(f"PubMed search failed: {e}")
|
| 186 |
-
return []
|
| 187 |
-
|
| 188 |
-
@staticmethod
|
| 189 |
-
def fetch_articles(pmids: List[str]) -> List[Dict[str, Any]]:
|
| 190 |
-
"""Fetch full article details for given PubMed IDs."""
|
| 191 |
-
if not pmids:
|
| 192 |
-
return []
|
| 193 |
-
|
| 194 |
-
ids = ",".join(pmids)
|
| 195 |
-
url = (
|
| 196 |
-
f"{PubMedClient.BASE_FETCH_URL}"
|
| 197 |
-
f"?db=pubmed&id={ids}&retmode=xml&rettype=abstract"
|
| 198 |
-
)
|
| 199 |
-
|
| 200 |
-
try:
|
| 201 |
-
response = fetch_with_retry(url)
|
| 202 |
-
response.raise_for_status()
|
| 203 |
-
return PubMedClient.parse_articles(response.text)
|
| 204 |
-
except requests.exceptions.RequestException as e:
|
| 205 |
-
logger.error(f"PubMed fetch failed: {e}")
|
| 206 |
-
return []
|
| 207 |
-
|
| 208 |
-
@staticmethod
|
| 209 |
-
def search_and_fetch(
|
| 210 |
-
keywords: List[str],
|
| 211 |
-
extra_terms: Optional[List[str]] = None,
|
| 212 |
-
retmax: int = 20,
|
| 213 |
-
) -> List[Dict[str, Any]]:
|
| 214 |
-
"""
|
| 215 |
-
Convenience method to search and fetch articles in one call.
|
| 216 |
-
"""
|
| 217 |
-
pmids = PubMedClient.pubmed_search(keywords, extra_terms, retmax)
|
| 218 |
-
if not pmids:
|
| 219 |
-
logger.info("No PubMed IDs found for search")
|
| 220 |
-
return []
|
| 221 |
-
|
| 222 |
-
return PubMedClient.fetch_articles(pmids)
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
class SemanticScholarClient:
|
| 226 |
-
"""Client for searching Semantic Scholar API."""
|
| 227 |
-
|
| 228 |
-
BASE_URL = "https://api.semanticscholar.org/graph/v1"
|
| 229 |
-
|
| 230 |
-
def __init__(self, api_key: Optional[str] = None):
|
| 231 |
-
self.api_key = api_key
|
| 232 |
-
self.headers = {}
|
| 233 |
-
if api_key:
|
| 234 |
-
self.headers["x-api-key"] = api_key
|
| 235 |
-
|
| 236 |
-
def search(self, query: str, max_results: int = 5) -> List[Dict[str, Any]]:
|
| 237 |
-
"""
|
| 238 |
-
Search Semantic Scholar for papers.
|
| 239 |
-
|
| 240 |
-
Args:
|
| 241 |
-
query: Search query string
|
| 242 |
-
max_results: Maximum number of results to return
|
| 243 |
-
|
| 244 |
-
Returns:
|
| 245 |
-
List of paper dictionaries with title, abstract, authors, etc.
|
| 246 |
-
"""
|
| 247 |
-
url = f"{self.BASE_URL}/paper/search"
|
| 248 |
-
params = {
|
| 249 |
-
"query": query,
|
| 250 |
-
"limit": max_results,
|
| 251 |
-
"fields": "title,abstract,authors,year,publicationDate,citationCount,url,externalIds"
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
try:
|
| 255 |
-
response = requests.get(
|
| 256 |
-
url,
|
| 257 |
-
params=params,
|
| 258 |
-
headers=self.headers,
|
| 259 |
-
timeout=10
|
| 260 |
-
)
|
| 261 |
-
response.raise_for_status()
|
| 262 |
-
data = response.json()
|
| 263 |
-
|
| 264 |
-
results = []
|
| 265 |
-
for paper in data.get("data", []):
|
| 266 |
-
results.append({
|
| 267 |
-
"title": paper.get("title", "Untitled"),
|
| 268 |
-
"abstract": paper.get("abstract", ""),
|
| 269 |
-
"authors": [author.get("name", "") for author in paper.get("authors", [])],
|
| 270 |
-
"year": paper.get("year"),
|
| 271 |
-
"published": paper.get("publicationDate", ""),
|
| 272 |
-
"citation_count": paper.get("citationCount", 0),
|
| 273 |
-
"url": paper.get("url", ""),
|
| 274 |
-
"doi": paper.get("externalIds", {}).get("DOI"),
|
| 275 |
-
"arxiv_id": paper.get("externalIds", {}).get("ArXiv"),
|
| 276 |
-
"pmid": paper.get("externalIds", {}).get("PubMed")
|
| 277 |
-
})
|
| 278 |
-
|
| 279 |
-
return results
|
| 280 |
-
|
| 281 |
-
except requests.exceptions.RequestException as e:
|
| 282 |
-
logger.error(f"Semantic Scholar search failed: {e}")
|
| 283 |
-
return []
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
class ArxivClient:
|
| 287 |
-
"""Client for searching arXiv papers."""
|
| 288 |
-
|
| 289 |
-
DEFAULT_KEYWORDS = [
|
| 290 |
-
'corvid', 'crow', 'raven', 'corvus', 'jay',
|
| 291 |
-
'magpie', 'jackdaw', 'rook', 'chough', 'nutcracker'
|
| 292 |
-
]
|
| 293 |
-
|
| 294 |
-
def __init__(self, default_keywords: Optional[List[str]] = None):
|
| 295 |
-
"""
|
| 296 |
-
Initialize ArxivClient.
|
| 297 |
-
|
| 298 |
-
Args:
|
| 299 |
-
default_keywords: List of keywords to include in searches.
|
| 300 |
-
If None, uses DEFAULT_KEYWORDS.
|
| 301 |
-
"""
|
| 302 |
-
self.default_keywords = default_keywords or self.DEFAULT_KEYWORDS
|
| 303 |
-
|
| 304 |
-
def search(
|
| 305 |
-
self,
|
| 306 |
-
query: str,
|
| 307 |
-
additional_keywords: Optional[List[str]] = None,
|
| 308 |
-
max_results: int = 5
|
| 309 |
-
) -> List[Dict[str, Any]]:
|
| 310 |
-
"""
|
| 311 |
-
Search arXiv for papers.
|
| 312 |
-
|
| 313 |
-
Args:
|
| 314 |
-
query: Main search query
|
| 315 |
-
additional_keywords: Keywords to OR with query. If None, uses default_keywords.
|
| 316 |
-
max_results: Maximum number of results to return
|
| 317 |
-
|
| 318 |
-
Returns:
|
| 319 |
-
List of paper dictionaries with title, abstract, authors, etc.
|
| 320 |
-
"""
|
| 321 |
-
keywords = additional_keywords if additional_keywords is not None else self.default_keywords
|
| 322 |
-
|
| 323 |
-
# build query: query OR keyword1 OR keyword2 ...
|
| 324 |
-
q_parts = [query] + keywords
|
| 325 |
-
q = " OR ".join(q_parts)
|
| 326 |
-
|
| 327 |
-
url = (
|
| 328 |
-
f"https://export.arxiv.org/api/query?"
|
| 329 |
-
f"search_query=all:({q})&start=0&max_results={max_results}&"
|
| 330 |
-
"sortBy=lastUpdatedDate&sortOrder=descending"
|
| 331 |
-
)
|
| 332 |
-
|
| 333 |
-
try:
|
| 334 |
-
response = requests.get(url, timeout=10)
|
| 335 |
-
response.raise_for_status()
|
| 336 |
-
feed = feedparser.parse(response.text)
|
| 337 |
-
|
| 338 |
-
results = []
|
| 339 |
-
for entry in feed.entries:
|
| 340 |
-
# Skip entries without abstracts
|
| 341 |
-
if not getattr(entry, "summary", "").strip():
|
| 342 |
-
continue
|
| 343 |
-
|
| 344 |
-
results.append({
|
| 345 |
-
"title": getattr(entry, "title", "Untitled"),
|
| 346 |
-
"abstract": getattr(entry, "summary", ""),
|
| 347 |
-
"authors": [a.name for a in getattr(entry, "authors", [])],
|
| 348 |
-
"published": getattr(entry, "published", ""),
|
| 349 |
-
"updated": getattr(entry, "updated", ""),
|
| 350 |
-
"arxiv_link": getattr(entry, "link", ""),
|
| 351 |
-
"arxiv_id": getattr(entry, "id", "").split("/abs/")[-1] if hasattr(entry, "id") else None,
|
| 352 |
-
"categories": [tag.term for tag in getattr(entry, "tags", [])]
|
| 353 |
-
})
|
| 354 |
-
|
| 355 |
-
return results
|
| 356 |
-
|
| 357 |
-
except requests.exceptions.RequestException as e:
|
| 358 |
-
logger.error(f"arXiv search failed: {e}")
|
| 359 |
-
return []
|
| 360 |
-
except Exception as e:
|
| 361 |
-
logger.error(f"Error parsing arXiv feed: {e}")
|
| 362 |
-
return []
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
# example usage
|
| 366 |
-
if __name__ == "__main__":
|
| 367 |
-
logging.basicConfig(level=logging.INFO)
|
| 368 |
-
|
| 369 |
-
# pubMed example
|
| 370 |
-
print("=== PubMed Search ===")
|
| 371 |
-
keywords = ['corvid', 'crow', 'raven']
|
| 372 |
-
extra = ['memory', 'cognition']
|
| 373 |
-
articles = PubMedClient.search_and_fetch(keywords, extra, retmax=5)
|
| 374 |
-
for article in articles:
|
| 375 |
-
print(f"\nTitle: {article['title']}")
|
| 376 |
-
print(f"Authors: {article['author_str']}")
|
| 377 |
-
print(f"DOI: {article.get('doi', 'N/A')}")
|
| 378 |
-
|
| 379 |
-
# arXiv example
|
| 380 |
-
print("\n\n=== arXiv Search ===")
|
| 381 |
-
arxiv = ArxivClient()
|
| 382 |
-
papers = arxiv.search("intelligence", max_results=3)
|
| 383 |
-
for paper in papers:
|
| 384 |
-
print(f"\nTitle: {paper['title']}")
|
| 385 |
-
print(f"Authors: {', '.join(paper['authors'][:3])}")
|
| 386 |
-
print(f"Link: {paper['arxiv_link']}")
|
| 387 |
-
|
| 388 |
-
# semantic Scholar example
|
| 389 |
-
print("\n\n=== Semantic Scholar Search ===")
|
| 390 |
-
ss = SemanticScholarClient()
|
| 391 |
-
papers = ss.search("corvid cognition", max_results=3)
|
| 392 |
-
for paper in papers:
|
| 393 |
-
print(f"\nTitle: {paper['title']}")
|
| 394 |
-
print(f"Citations: {paper['citation_count']}")
|
| 395 |
-
print(f"Year: {paper['year']}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ticket_list.md
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
# Ticket List for Cluas
|
| 2 |
-
|
| 3 |
-
This document outlines suggested tasks to improve the Cluas project, aimed at a junior to mid-level engineer.
|
| 4 |
-
|
| 5 |
-
## Core Improvements
|
| 6 |
-
|
| 7 |
-
These tickets address foundational aspects of the project to improve its robustness, maintainability, and developer experience.
|
| 8 |
-
|
| 9 |
-
- **TICKET-01: Implement a Linter and Formatter**
|
| 10 |
-
- **Description**: The project currently lacks automated code linting and formatting. Introduce a tool like `ruff` to enforce a consistent code style and catch potential errors.
|
| 11 |
-
- **Tasks**:
|
| 12 |
-
1. Add `ruff` to the project dependencies in `pyproject.toml`.
|
| 13 |
-
2. Create a `ruff.toml` or `pyproject.toml` configuration file with basic rules.
|
| 14 |
-
3. Run `ruff format .` and `ruff check --fix .` to format the existing codebase.
|
| 15 |
-
4. Update the `README.md` with instructions on how to run the linter.
|
| 16 |
-
|
| 17 |
-
- **TICKET-02: Expand Test Coverage for Entrypoints**
|
| 18 |
-
- **Description**: The `tests/integration` directory only contains tests for `academic_search_entrypoint`. Similar tests should be created for the other entrypoints to ensure they work as expected.
|
| 19 |
-
- **Tasks**:
|
| 20 |
-
1. Create `test_news_search_entrypoint.py` in `tests/integration/`.
|
| 21 |
-
2. Create `test_observation_entrypoint.py` in `tests/integration/`.
|
| 22 |
-
3. Create `test_web_search_entrypoint.py` in `tests/integration/`.
|
| 23 |
-
4. Write basic integration tests for each entrypoint, mocking the external API calls.
|
| 24 |
-
|
| 25 |
-
- **TICKET-03: Add Type Hinting**
|
| 26 |
-
- **Description**: While some parts of the code use type hints, many functions are missing them. Gradually adding type hints will improve code clarity and allow for static analysis.
|
| 27 |
-
- **Tasks**:
|
| 28 |
-
1. Start with the files in `src/cluas_mcp/common/` and add type hints to all function signatures and variables.
|
| 29 |
-
2. Continue adding type hints to the entrypoint files in `src/cluas_mcp/`.
|
| 30 |
-
|
| 31 |
-
- **TICKET-04: Improve the README.md**
|
| 32 |
-
- **Description**: The `README.md` provides a good overview, but it could be improved with more practical information for developers.
|
| 33 |
-
- **Tasks**:
|
| 34 |
-
1. Add an "Installation" section with instructions on how to set up the project and install dependencies (e.g., using `uv`).
|
| 35 |
-
2. Add a "Running the Application" section that explains how to start the Gradio app.
|
| 36 |
-
3. Add a "Running Tests" section that consolidates the test commands from the bottom of the file.
|
| 37 |
-
|
| 38 |
-
- **TICKET-05: Refactor or Remove `thing.py`**
|
| 39 |
-
- **Description**: The file `src/cluas_mcp/academic/thing.py` seems to be a temporary or test script. It should be either removed or refactored into a meaningful module.
|
| 40 |
-
- **Tasks**:
|
| 41 |
-
1. Analyze the purpose of the `print` statement in `thing.py`.
|
| 42 |
-
2. If it's a leftover test script, delete the file.
|
| 43 |
-
3. If it serves a purpose, rename the file to something descriptive and integrate it properly.
|
| 44 |
-
|
| 45 |
-
## Further Ideas
|
| 46 |
-
|
| 47 |
-
These are suggestions for new features or major improvements that could be implemented after the core improvements are complete.
|
| 48 |
-
|
| 49 |
-
- **IDEA-01: Implement the Orchestrator**
|
| 50 |
-
- **Description**: The `src/orchestrator.py` file is currently a placeholder. Implementing it would be the next major step towards the project's vision of a dialectic research tool.
|
| 51 |
-
- **Tasks**:
|
| 52 |
-
1. Design the `Orchestrator` class structure.
|
| 53 |
-
2. Implement logic to pass user queries to the relevant characters.
|
| 54 |
-
3. Develop a system for synthesizing responses from multiple characters.
|
| 55 |
-
4. Integrate the orchestrator with the Gradio app.
|
| 56 |
-
|
| 57 |
-
- **IDEA-02: Create a Dockerfile**
|
| 58 |
-
- **Description**: Containerizing the application with Docker would make it easier to deploy and run in a consistent environment.
|
| 59 |
-
- **Tasks**:
|
| 60 |
-
1. Create a `Dockerfile` that installs Python, copies the project files, and installs dependencies.
|
| 61 |
-
2. Add a `docker-compose.yml` file for easier local development.
|
| 62 |
-
|
| 63 |
-
- **IDEA-03: Set Up a CI/CD Pipeline**
|
| 64 |
-
- **Description**: A simple CI/CD pipeline (e.g., using GitHub Actions) could automatically run tests and linting on every push or pull request.
|
| 65 |
-
- **Tasks**:
|
| 66 |
-
1. Create a `.github/workflows/ci.yml` file.
|
| 67 |
-
2. Define a workflow that runs `ruff check .` and `pytest`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|