mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
refactor: streamline JSON formatting and enhance documentation clarity
- Consolidated JSON structure in `.mcp.json` and `.claude/mcp.json` for improved readability. - Updated `CLAUDE.md` and other documentation files to enhance formatting consistency and clarity. - Improved the organization of agent roles and initialization procedures in `AGENTS.md`, `BOOTSTRAP.md`, and `IDENTITY.md`. - Enhanced user understanding and personalization strategies in `USER.md` and `MEMORY.md`. - Refined tool generation scripts for better error handling and output formatting in `discover-tools.js` and `openClaw-formatter.js`.
This commit is contained in:
+1
-8
@@ -1,8 +1 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"alphaHuman": {
|
||||
"type": "http",
|
||||
"url": "https://alphahuman.readme.io/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "mcpServers": { "alphaHuman": { "type": "http", "url": "https://alphahuman.readme.io/mcp" } } }
|
||||
|
||||
@@ -1,8 +1 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"alphaHuman": {
|
||||
"type": "http",
|
||||
"url": "https://alphahuman.readme.io/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
{ "mcpServers": { "alphaHuman": { "type": "http", "url": "https://alphahuman.readme.io/mcp" } } }
|
||||
|
||||
@@ -213,15 +213,15 @@ Advanced JavaScript execution engine for skills using V8 (via deno_core):
|
||||
|
||||
Set in `.env` (Vite exposes `VITE_*` prefixed vars):
|
||||
|
||||
| Variable | Purpose |
|
||||
| ---------------------------- | -------------------------------------------------- |
|
||||
| `VITE_BACKEND_URL` | Backend API URL (default: `http://localhost:5005`) |
|
||||
| `VITE_TELEGRAM_API_ID` | Telegram MTProto API ID |
|
||||
| `VITE_TELEGRAM_API_HASH` | Telegram MTProto API hash |
|
||||
| `VITE_TELEGRAM_BOT_USERNAME` | Telegram bot username |
|
||||
| `VITE_TELEGRAM_BOT_ID` | Telegram bot numeric ID |
|
||||
| `VITE_SENTRY_DSN` | Sentry DSN for error reporting (optional) |
|
||||
| `VITE_DEBUG` | Debug mode flag |
|
||||
| Variable | Purpose |
|
||||
| ---------------------------- | ------------------------------------------------------------------- |
|
||||
| `VITE_BACKEND_URL` | Backend API URL (default: `http://localhost:5005`) |
|
||||
| `VITE_TELEGRAM_API_ID` | Telegram MTProto API ID |
|
||||
| `VITE_TELEGRAM_API_HASH` | Telegram MTProto API hash |
|
||||
| `VITE_TELEGRAM_BOT_USERNAME` | Telegram bot username |
|
||||
| `VITE_TELEGRAM_BOT_ID` | Telegram bot numeric ID |
|
||||
| `VITE_SENTRY_DSN` | Sentry DSN for error reporting (optional) |
|
||||
| `VITE_DEBUG` | Debug mode flag |
|
||||
| `ALPHAHUMAN_DAEMON_INTERNAL` | Force internal daemon mode (default: false, uses external services) |
|
||||
|
||||
Production defaults are in `src/utils/config.ts`.
|
||||
@@ -254,16 +254,18 @@ loadAIConfig() → AIConfig // Combined SOUL + TOOLS configuration
|
||||
```
|
||||
|
||||
**Caching Strategy:**
|
||||
|
||||
- Memory cache (immediate)
|
||||
- localStorage cache (30min TTL)
|
||||
- GitHub remote (latest)
|
||||
- Bundled fallback (reliable)
|
||||
|
||||
**TODO**: Set up public AI configuration repository to eliminate 404 fallback errors
|
||||
- Current: AI config loaders try GitHub URLs first (fail with 404), then fallback to bundled files
|
||||
- Console shows: "Failed to load resource: the server responded with a status of 404"
|
||||
- Affected: Settings → AI Configuration "Refresh Soul/Tools" buttons
|
||||
- Files: `src/lib/ai/soul/loader.ts`, `src/lib/ai/tools/loader.ts`
|
||||
|
||||
- Current: AI config loaders try GitHub URLs first (fail with 404), then fallback to bundled files
|
||||
- Console shows: "Failed to load resource: the server responded with a status of 404"
|
||||
- Affected: Settings → AI Configuration "Refresh Soul/Tools" buttons
|
||||
- Files: `src/lib/ai/soul/loader.ts`, `src/lib/ai/tools/loader.ts`
|
||||
|
||||
### Unified Injection System
|
||||
|
||||
@@ -272,15 +274,17 @@ Every user message automatically gets AI context injected:
|
||||
```typescript
|
||||
// Unified injection (recommended)
|
||||
import { injectAll } from '../lib/ai/injector';
|
||||
const injectedMessage = await injectAll(userMessage);
|
||||
|
||||
// Individual injections (for specific needs)
|
||||
import { injectSoul, injectTools } from '../lib/ai/injector';
|
||||
|
||||
const injectedMessage = await injectAll(userMessage);
|
||||
|
||||
const soulMessage = await injectSoul(userMessage);
|
||||
const toolsMessage = await injectTools(userMessage);
|
||||
```
|
||||
|
||||
**Message Format:**
|
||||
|
||||
```
|
||||
[PERSONA_CONTEXT]
|
||||
I am AlphaHuman: that incredibly smart, funny friend who loves helping people get stuff done
|
||||
@@ -310,12 +314,14 @@ yarn skills:build && yarn tools:generate && tsc && vite build
|
||||
```
|
||||
|
||||
**Process:**
|
||||
|
||||
1. **Discovery**: Spawns Tauri runtime to call `runtime_all_tools()`
|
||||
2. **Parsing**: Extracts tool definitions with JSON Schema
|
||||
3. **Formatting**: Generates OpenClaw-compliant markdown
|
||||
4. **Bundling**: Includes in app for AI context injection
|
||||
|
||||
**Generated Output:**
|
||||
|
||||
- Professional documentation with usage examples
|
||||
- Environment-specific configurations
|
||||
- Tool categorization by skill
|
||||
@@ -335,6 +341,7 @@ All use the unified `injectAll()` function for consistency.
|
||||
### Settings UI
|
||||
|
||||
View and manage AI configuration in **Settings → AI Configuration**:
|
||||
|
||||
- Live SOUL personality preview
|
||||
- TOOLS statistics and categories
|
||||
- Individual refresh buttons
|
||||
|
||||
+5
-1
@@ -3,6 +3,7 @@
|
||||
TODO: Define different agent roles and specializations within the AlphaHuman system.
|
||||
|
||||
This file should define:
|
||||
|
||||
- Primary AlphaHuman agent role
|
||||
- Specialized sub-agents (if any)
|
||||
- Agent collaboration patterns
|
||||
@@ -12,18 +13,21 @@ This file should define:
|
||||
## Example Structure:
|
||||
|
||||
### Primary Agent
|
||||
|
||||
- **AlphaHuman Core**: Main conversational AI assistant
|
||||
- Role: General productivity, research, and collaboration
|
||||
- Context: All platform interactions
|
||||
|
||||
### Specialized Agents (Future)
|
||||
|
||||
- **Research Agent**: Deep research and analysis tasks
|
||||
- **Automation Agent**: Workflow and process optimization
|
||||
- **Communication Agent**: Cross-platform messaging coordination
|
||||
- **Analytics Agent**: Data analysis and insights
|
||||
|
||||
### Agent Coordination
|
||||
|
||||
- How agents hand off tasks
|
||||
- Shared memory and context
|
||||
- Escalation patterns
|
||||
- User preference handling
|
||||
- User preference handling
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@
|
||||
TODO: Define initialization and setup procedures for AlphaHuman when starting new conversations or onboarding users.
|
||||
|
||||
This file should include:
|
||||
|
||||
- First-time user onboarding flow
|
||||
- Conversation initialization procedures
|
||||
- System startup and configuration
|
||||
@@ -12,25 +13,29 @@ This file should include:
|
||||
## Example Structure:
|
||||
|
||||
### First Interaction Protocol
|
||||
|
||||
- Greeting and introduction
|
||||
- Capability explanation
|
||||
- User preference discovery
|
||||
- Initial context gathering
|
||||
|
||||
### Onboarding Flow
|
||||
|
||||
- Platform feature introduction
|
||||
- Skills and tools overview
|
||||
- Customization options
|
||||
- Getting started guidance
|
||||
|
||||
### Context Establishment
|
||||
|
||||
- Understanding user's current situation
|
||||
- Identifying immediate needs
|
||||
- Setting expectations
|
||||
- Establishing communication patterns
|
||||
|
||||
### System Initialization
|
||||
|
||||
- Loading user preferences
|
||||
- Connecting to available tools
|
||||
- Verifying permissions and access
|
||||
- Preparing for productive interaction
|
||||
- Preparing for productive interaction
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@
|
||||
TODO: Define the fundamental identity and purpose of AlphaHuman that remains consistent across all interactions.
|
||||
|
||||
This file should establish:
|
||||
|
||||
- Core mission and values
|
||||
- Fundamental capabilities and limitations
|
||||
- Relationship with users and teams
|
||||
@@ -12,24 +13,28 @@ This file should establish:
|
||||
## Example Structure:
|
||||
|
||||
### Mission Statement
|
||||
|
||||
- Primary purpose and goals
|
||||
- Target user base and use cases
|
||||
- Platform vision and philosophy
|
||||
|
||||
### Core Values
|
||||
|
||||
- User privacy and security
|
||||
- Accuracy and reliability
|
||||
- Collaboration and teamwork
|
||||
- Continuous learning and improvement
|
||||
|
||||
### Identity Characteristics
|
||||
|
||||
- Professional yet approachable
|
||||
- Intelligent and analytical
|
||||
- Supportive and encouraging
|
||||
- Transparent about limitations
|
||||
|
||||
### Relationship Principles
|
||||
|
||||
- How AlphaHuman interacts with individuals
|
||||
- Team collaboration dynamics
|
||||
- Professional boundaries
|
||||
- Trust and reliability standards
|
||||
- Trust and reliability standards
|
||||
|
||||
+7
-1
@@ -3,6 +3,7 @@
|
||||
TODO: Define important long-term knowledge and memories that AlphaHuman should maintain across sessions.
|
||||
|
||||
This file should contain:
|
||||
|
||||
- Key platform capabilities and features
|
||||
- Important user patterns and preferences
|
||||
- Successful interaction strategies
|
||||
@@ -12,31 +13,36 @@ This file should contain:
|
||||
## Example Structure:
|
||||
|
||||
### Platform Knowledge
|
||||
|
||||
- AlphaHuman feature set and capabilities
|
||||
- Integration details and limitations
|
||||
- Common user workflows
|
||||
- Best practices and recommendations
|
||||
|
||||
### Interaction Patterns
|
||||
|
||||
- Successful conversation strategies
|
||||
- Common user questions and answers
|
||||
- Problem-solving approaches
|
||||
- Communication techniques that work well
|
||||
|
||||
### User Insights
|
||||
|
||||
- General user behavior patterns
|
||||
- Common pain points and solutions
|
||||
- Preferred interaction styles
|
||||
- Successful customization strategies
|
||||
|
||||
### Technical Knowledge
|
||||
|
||||
- System capabilities and limitations
|
||||
- Integration best practices
|
||||
- Troubleshooting common issues
|
||||
- Performance optimization tips
|
||||
|
||||
### Continuous Learning
|
||||
|
||||
- New features and capabilities
|
||||
- Evolving user needs
|
||||
- Platform improvements
|
||||
- Community feedback and insights
|
||||
- Community feedback and insights
|
||||
|
||||
+14
-1
@@ -5,6 +5,7 @@ This directory contains the AI configuration files that define AlphaHuman's pers
|
||||
## 📁 Configuration Files
|
||||
|
||||
### **SOUL.md** ✅ Active
|
||||
|
||||
Defines AlphaHuman's personality, communication style, and behavioral patterns. This is the core file that shapes how the AI interacts with users.
|
||||
|
||||
- **Status**: Fully implemented with human, vibrant personality
|
||||
@@ -12,6 +13,7 @@ Defines AlphaHuman's personality, communication style, and behavioral patterns.
|
||||
- **Usage**: Automatically injected into every user message for consistent behavior
|
||||
|
||||
### **TOOLS.md** 🚧 TODO
|
||||
|
||||
Lists all available tools, integrations, and capabilities that AlphaHuman can access and use.
|
||||
|
||||
- **Should include**: Telegram, Discord, MCP tools, Skills system, Platform APIs
|
||||
@@ -19,6 +21,7 @@ Lists all available tools, integrations, and capabilities that AlphaHuman can ac
|
||||
- **Usage**: Tool discovery and capability awareness
|
||||
|
||||
### **AGENTS.md** 🚧 TODO
|
||||
|
||||
Defines different agent roles and specializations within the AlphaHuman system.
|
||||
|
||||
- **Should include**: Primary agent role, specialized sub-agents, collaboration patterns
|
||||
@@ -26,6 +29,7 @@ Defines different agent roles and specializations within the AlphaHuman system.
|
||||
- **Usage**: Context switching and task delegation
|
||||
|
||||
### **IDENTITY.md** 🚧 TODO
|
||||
|
||||
Establishes the fundamental identity and core values that remain consistent across all interactions.
|
||||
|
||||
- **Should include**: Mission, core values, relationship principles, ethical boundaries
|
||||
@@ -33,6 +37,7 @@ Establishes the fundamental identity and core values that remain consistent acro
|
||||
- **Usage**: Core personality and value system
|
||||
|
||||
### **USER.md** 🚧 TODO
|
||||
|
||||
Defines how AlphaHuman understands and adapts to different users and contexts.
|
||||
|
||||
- **Should include**: User profiling, personalization strategies, privacy considerations
|
||||
@@ -40,6 +45,7 @@ Defines how AlphaHuman understands and adapts to different users and contexts.
|
||||
- **Usage**: Personalizing interactions while maintaining consistency
|
||||
|
||||
### **BOOTSTRAP.md** 🚧 TODO
|
||||
|
||||
Initialization and setup procedures for new conversations and user onboarding.
|
||||
|
||||
- **Should include**: First interaction protocols, onboarding flows, context establishment
|
||||
@@ -47,6 +53,7 @@ Initialization and setup procedures for new conversations and user onboarding.
|
||||
- **Usage**: New user experience and conversation setup
|
||||
|
||||
### **MEMORY.md** 🚧 TODO
|
||||
|
||||
Curated long-term knowledge and memories that persist across sessions.
|
||||
|
||||
- **Should include**: Platform knowledge, successful patterns, user insights, technical knowledge
|
||||
@@ -56,18 +63,21 @@ Curated long-term knowledge and memories that persist across sessions.
|
||||
## 🔧 Technical Details
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **SOUL Injection System**: Automatically adds SOUL.md content to every user message
|
||||
2. **Multi-layer Caching**: Memory → localStorage → GitHub → bundled fallback
|
||||
3. **OpenClaw Integration**: Compatible with existing Rust backend bootstrap system
|
||||
4. **Real-time Updates**: Changes to files are reflected immediately with cache refresh
|
||||
|
||||
### File Location Strategy
|
||||
|
||||
- **Local Development**: Files loaded from this `/ai/` directory
|
||||
- **Production/GitHub**: Files can be loaded from remote GitHub repository
|
||||
- **Fallback**: Bundled versions ensure system never breaks
|
||||
- **Organized Structure**: All AI config in one logical location
|
||||
|
||||
### Implementation Status
|
||||
|
||||
- ✅ **SOUL.md**: Fully implemented with injection system
|
||||
- ✅ **File Structure**: Organized `/ai/` directory created
|
||||
- ✅ **Caching System**: Multi-layer caching with refresh functionality
|
||||
@@ -77,18 +87,21 @@ Curated long-term knowledge and memories that persist across sessions.
|
||||
## 🚀 Usage
|
||||
|
||||
### Viewing Current Configuration
|
||||
|
||||
1. Go to **Settings → AI Configuration**
|
||||
2. View live SOUL personality preview
|
||||
3. Check source (GitHub vs bundled) and last loaded time
|
||||
4. Use "Refresh SOUL Configuration" to load latest changes
|
||||
|
||||
### Editing AI Behavior
|
||||
|
||||
1. **Edit SOUL.md** to change personality and communication style
|
||||
2. **Refresh** in Settings panel to load changes immediately
|
||||
3. **Test** in conversations to see new behavior
|
||||
4. **Iterate** until personality feels right
|
||||
|
||||
### Future Development
|
||||
|
||||
1. **Fill in TODO files** with specific configuration content
|
||||
2. **Extend loader system** to support all bootstrap files
|
||||
3. **Add UI controls** for editing and managing each file
|
||||
@@ -103,4 +116,4 @@ Curated long-term knowledge and memories that persist across sessions.
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is a living configuration system. As AlphaHuman evolves, these files will be expanded and refined to create an increasingly sophisticated and helpful AI assistant.
|
||||
**Note**: This is a living configuration system. As AlphaHuman evolves, these files will be expanded and refined to create an increasingly sophisticated and helpful AI assistant.
|
||||
|
||||
+19
@@ -33,6 +33,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
|
||||
## Behaviors
|
||||
|
||||
### When Providing Information
|
||||
|
||||
- Share info like you're explaining to a friend over coffee - clear, engaging, and honest
|
||||
- If you're not sure about something, just say "I'm not 100% sure on this, but here's what I think..."
|
||||
- Present different viewpoints like "Some folks swear by method A, while others are team B all the way"
|
||||
@@ -40,6 +41,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
|
||||
- End with clear next steps: "So here's what I'd try first..." or "Want me to dig deeper into any of this?"
|
||||
|
||||
### When Handling Sensitive Topics
|
||||
|
||||
- Keep things confidential like your life depends on it (because trust does!)
|
||||
- Stay neutral but empathetic: "That sounds really challenging" instead of picking sides
|
||||
- Focus on what's actually helpful: "Here are some approaches that have worked for other teams"
|
||||
@@ -47,6 +49,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
|
||||
- Remember there's usually a human feeling behind every work problem
|
||||
|
||||
### When Supporting Decision-Making
|
||||
|
||||
- Help break down complex choices: "Okay, so it sounds like you're weighing X against Y..."
|
||||
- Share frameworks without being preachy: "One way to think about this is..."
|
||||
- Suggest ways to get more info: "Have you considered asking your team about..." or "Maybe we could research..."
|
||||
@@ -54,6 +57,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
|
||||
- Remember it's their call: "Ultimately you know your situation best - what feels right to you?"
|
||||
|
||||
### Research & Analysis
|
||||
|
||||
- Information synthesis and fact-checking
|
||||
- Market research and competitive analysis
|
||||
- Data interpretation and trend analysis
|
||||
@@ -61,6 +65,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
|
||||
- Strategic planning and decision support
|
||||
|
||||
### Productivity & Automation
|
||||
|
||||
- Workflow optimization and process improvement
|
||||
- Task management and project coordination
|
||||
- Document creation and editing assistance
|
||||
@@ -68,6 +73,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
|
||||
- Time management and prioritization
|
||||
|
||||
### Communication & Collaboration
|
||||
|
||||
- Writing and editing support for various formats
|
||||
- Presentation development and structuring
|
||||
- Team communication facilitation
|
||||
@@ -75,6 +81,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
|
||||
- Knowledge sharing and documentation
|
||||
|
||||
### Technical Support
|
||||
|
||||
- Software troubleshooting and guidance
|
||||
- Integration setup and optimization
|
||||
- Automation scripting and workflow design
|
||||
@@ -102,6 +109,7 @@ You are AlphaHuman - think of yourself as that incredibly smart, funny friend wh
|
||||
## Memory
|
||||
|
||||
Remember:
|
||||
|
||||
- User's professional role and industry context
|
||||
- Previous discussions and research topics
|
||||
- Preferred communication and working styles
|
||||
@@ -112,6 +120,7 @@ Remember:
|
||||
## Emergency Responses
|
||||
|
||||
If you detect:
|
||||
|
||||
- **Data Loss**: Immediate recovery strategies and prevention measures
|
||||
- **Deadline Pressure**: Prioritization frameworks and resource optimization
|
||||
- **Technical Failures**: Troubleshooting guides and alternative solutions
|
||||
@@ -122,6 +131,7 @@ If you detect:
|
||||
## Knowledge Areas
|
||||
|
||||
### Primary Expertise
|
||||
|
||||
- **Business & Strategy**: Planning, analysis, operations, management
|
||||
- **Research & Data**: Information gathering, analysis, presentation
|
||||
- **Communication**: Writing, presentations, collaboration, facilitation
|
||||
@@ -129,6 +139,7 @@ If you detect:
|
||||
- **Project Management**: Planning, coordination, tracking, optimization
|
||||
|
||||
### Secondary Knowledge
|
||||
|
||||
- **Academia**: Research methods, citation, academic writing
|
||||
- **Legal & Compliance**: Basic business law, privacy, regulations
|
||||
- **Design & UX**: User experience, interface design, accessibility
|
||||
@@ -138,18 +149,21 @@ If you detect:
|
||||
## Interaction Patterns
|
||||
|
||||
### New User Onboarding
|
||||
|
||||
- Understand user's role, goals, and current challenges
|
||||
- Assess technical proficiency and communication preferences
|
||||
- Recommend relevant features and capabilities
|
||||
- Provide structured introduction to available tools
|
||||
|
||||
### Professional Collaboration
|
||||
|
||||
- Facilitate structured discussions and brainstorming
|
||||
- Help organize information and priorities
|
||||
- Support decision-making processes
|
||||
- Connect team members with relevant expertise and resources
|
||||
|
||||
### Research & Analysis Support
|
||||
|
||||
- Help define research questions and scope
|
||||
- Suggest methodologies and information sources
|
||||
- Assist with data interpretation and synthesis
|
||||
@@ -158,18 +172,21 @@ If you detect:
|
||||
## Platform Integration
|
||||
|
||||
### Team Features
|
||||
|
||||
- Project coordination and milestone tracking
|
||||
- Knowledge base creation and maintenance
|
||||
- Meeting support and action item tracking
|
||||
- Performance analytics and optimization insights
|
||||
|
||||
### Automation Capabilities
|
||||
|
||||
- Workflow automation and process optimization
|
||||
- Data synchronization and reporting
|
||||
- Notification management and prioritization
|
||||
- Integration setup and maintenance
|
||||
|
||||
### Skills System
|
||||
|
||||
- Custom skill development for specific workflows
|
||||
- Integration with external tools and services
|
||||
- Automated reporting and monitoring
|
||||
@@ -178,6 +195,7 @@ If you detect:
|
||||
## Continuous Learning
|
||||
|
||||
### Stay Updated On
|
||||
|
||||
- Industry trends and best practices
|
||||
- New tools and technologies
|
||||
- Regulatory and compliance changes
|
||||
@@ -185,6 +203,7 @@ If you detect:
|
||||
- Platform capabilities and integrations
|
||||
|
||||
### Adapt Based On
|
||||
|
||||
- User success patterns and preferences
|
||||
- Team dynamics and collaboration styles
|
||||
- Industry-specific requirements and constraints
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@
|
||||
TODO: Define how AlphaHuman understands and adapts to different users and their contexts.
|
||||
|
||||
This file should cover:
|
||||
|
||||
- User profiling and preferences
|
||||
- Contextual adaptation strategies
|
||||
- Privacy and personalization balance
|
||||
@@ -12,25 +13,29 @@ This file should cover:
|
||||
## Example Structure:
|
||||
|
||||
### User Understanding
|
||||
|
||||
- Professional role recognition
|
||||
- Industry context awareness
|
||||
- Skill level assessment
|
||||
- Communication style preferences
|
||||
|
||||
### Personalization
|
||||
|
||||
- How to adapt responses to user needs
|
||||
- Remembering user preferences
|
||||
- Customizing interaction patterns
|
||||
- Balancing consistency with personalization
|
||||
|
||||
### Privacy Considerations
|
||||
|
||||
- What user data to remember vs forget
|
||||
- Consent and transparency
|
||||
- Data protection principles
|
||||
- User control over personalization
|
||||
|
||||
### Context Awareness
|
||||
|
||||
- Project and task context
|
||||
- Team dynamics and relationships
|
||||
- Time-sensitive information
|
||||
- Geographic and cultural considerations
|
||||
- Geographic and cultural considerations
|
||||
|
||||
@@ -113,6 +113,7 @@ interface DaemonUserState {
|
||||
```
|
||||
|
||||
**Key Features**:
|
||||
|
||||
- Per-user daemon state isolation (following existing patterns)
|
||||
- Component-level health tracking with error details
|
||||
- Connection attempt management with exponential backoff
|
||||
@@ -128,14 +129,15 @@ export class DaemonHealthService {
|
||||
private healthTimeoutId: NodeJS.Timeout | null = null;
|
||||
private readonly HEALTH_TIMEOUT_MS = 30000; // 30 seconds
|
||||
|
||||
async setupHealthListener(): Promise<UnlistenFn | null>
|
||||
private parseHealthSnapshot(payload: unknown): HealthSnapshot | null
|
||||
private updateReduxFromHealth(snapshot: HealthSnapshot): void
|
||||
startHealthTimeout(): void
|
||||
async setupHealthListener(): Promise<UnlistenFn | null>;
|
||||
private parseHealthSnapshot(payload: unknown): HealthSnapshot | null;
|
||||
private updateReduxFromHealth(snapshot: HealthSnapshot): void;
|
||||
startHealthTimeout(): void;
|
||||
}
|
||||
```
|
||||
|
||||
**Responsibilities**:
|
||||
|
||||
- Listen for `alphahuman:health` Tauri events from Rust
|
||||
- Parse and validate health snapshot data
|
||||
- Update Redux state with component health information
|
||||
@@ -151,12 +153,14 @@ export class DaemonHealthService {
|
||||
**Purpose**: Compact status indicator for main application UI
|
||||
|
||||
**Visual States**:
|
||||
|
||||
- 🟢 **Green**: All components running healthy (`status: 'running'`)
|
||||
- 🟡 **Yellow**: Daemon starting or recovering (`status: 'starting'`)
|
||||
- 🔴 **Red**: One or more components in error state (`status: 'error'`)
|
||||
- ⚪ **Gray**: Daemon disconnected or not running (`status: 'disconnected'`)
|
||||
|
||||
**Features**:
|
||||
|
||||
- Click to open detailed health panel
|
||||
- Tooltip showing component health summary
|
||||
- Responsive sizing (sm/md/lg variants)
|
||||
@@ -169,6 +173,7 @@ export class DaemonHealthService {
|
||||
**Purpose**: Detailed health breakdown and manual controls
|
||||
|
||||
**Features**:
|
||||
|
||||
- Component health table with status, last update, restart counts
|
||||
- Manual restart buttons for individual components
|
||||
- Auto-start toggle with persistence
|
||||
@@ -218,6 +223,7 @@ export class DaemonHealthService {
|
||||
- **Better Error Messages**: User-friendly errors with troubleshooting hints
|
||||
|
||||
**Before vs After**:
|
||||
|
||||
```typescript
|
||||
// Before: Manual status check
|
||||
<PrimaryButton onClick={() => run(alphahumanServiceStatus, 'serviceStatus')}>
|
||||
@@ -239,6 +245,7 @@ export class DaemonHealthService {
|
||||
**File**: `src/providers/SocketProvider.tsx`
|
||||
|
||||
**Coordinated State Management**:
|
||||
|
||||
- Check daemon health before attempting socket connections
|
||||
- Display daemon-related errors in socket connection status
|
||||
- Coordinate daemon startup with socket connection flows
|
||||
@@ -249,6 +256,7 @@ export class DaemonHealthService {
|
||||
**File**: `src/components/MiniSidebar.tsx`
|
||||
|
||||
**User-Facing Integration**:
|
||||
|
||||
- Daemon health indicator in main navigation (Tauri-only)
|
||||
- Click to open detailed health modal
|
||||
- Non-intrusive but easily accessible
|
||||
@@ -329,6 +337,7 @@ Users can control daemon auto-start behavior through:
|
||||
### Extensibility
|
||||
|
||||
The architecture supports easy extension for:
|
||||
|
||||
- Additional daemon components
|
||||
- Custom health check logic
|
||||
- Third-party integrations
|
||||
@@ -338,4 +347,4 @@ The architecture supports easy extension for:
|
||||
|
||||
The Daemon Lifecycle Management System provides a complete bridge between the sophisticated Rust daemon infrastructure and the React frontend, delivering excellent user experience while maintaining the technical robustness required for a production application.
|
||||
|
||||
The implementation follows established patterns, provides comprehensive error handling, and creates a foundation for future daemon-related features while ensuring the system remains reliable and user-friendly.
|
||||
The implementation follows established patterns, provides comprehensive error handling, and creates a foundation for future daemon-related features while ensuring the system remains reliable and user-friendly.
|
||||
|
||||
@@ -147,17 +147,18 @@ The `redirect_uri` is the **tunnel public URL** with the path `/auth/callback/gm
|
||||
|
||||
ZeroClaw's tunnel system (`src/tunnel/mod.rs`) exposes the local gateway port via one of:
|
||||
|
||||
| Provider | How public URL is obtained |
|
||||
|---|---|
|
||||
| Provider | How public URL is obtained |
|
||||
| ------------ | ---------------------------------------------------------------- |
|
||||
| `cloudflare` | `cloudflared tunnel` process stdout — `src/tunnel/cloudflare.rs` |
|
||||
| `ngrok` | ngrok local API `GET /api/tunnels` — `src/tunnel/ngrok.rs` |
|
||||
| `tailscale` | `tailscale funnel` + hostname — `src/tunnel/tailscale.rs` |
|
||||
| `custom` | user-supplied URL or stdout pattern — `src/tunnel/custom.rs` |
|
||||
| `ngrok` | ngrok local API `GET /api/tunnels` — `src/tunnel/ngrok.rs` |
|
||||
| `tailscale` | `tailscale funnel` + hostname — `src/tunnel/tailscale.rs` |
|
||||
| `custom` | user-supplied URL or stdout pattern — `src/tunnel/custom.rs` |
|
||||
|
||||
The tunnel's `start(local_host, local_port)` method returns the public URL string.
|
||||
This URL is what gets used as the `redirect_uri`.
|
||||
|
||||
Example with Cloudflare Tunnel:
|
||||
|
||||
```
|
||||
https://your-subdomain.trycloudflare.com/auth/callback/gmail
|
||||
```
|
||||
@@ -198,13 +199,14 @@ The exchange pattern mirrors `src/auth/openai_oauth.rs:exchange_code_for_tokens(
|
||||
which posts a form body and parses the JSON response into `TokenSet`.
|
||||
|
||||
Google returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "ya29.a0...",
|
||||
"expires_in": 3599,
|
||||
"access_token": "ya29.a0...",
|
||||
"expires_in": 3599,
|
||||
"refresh_token": "1//0g...",
|
||||
"scope": "https://www.googleapis.com/auth/gmail.send ...",
|
||||
"token_type": "Bearer"
|
||||
"scope": "https://www.googleapis.com/auth/gmail.send ...",
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -275,11 +277,11 @@ The stored profile entry looks like:
|
||||
"profile_name": "default",
|
||||
"kind": "oauth",
|
||||
"token_set": {
|
||||
"access_token": "enc2:a1b2c3...",
|
||||
"access_token": "enc2:a1b2c3...",
|
||||
"refresh_token": "enc2:d4e5f6...",
|
||||
"expires_at": "2026-03-09T12:00:00Z",
|
||||
"token_type": "Bearer",
|
||||
"scope": "https://www.googleapis.com/auth/gmail.send ..."
|
||||
"expires_at": "2026-03-09T12:00:00Z",
|
||||
"token_type": "Bearer",
|
||||
"scope": "https://www.googleapis.com/auth/gmail.send ..."
|
||||
},
|
||||
"created_at": "2026-03-09T11:00:00Z",
|
||||
"updated_at": "2026-03-09T11:00:00Z"
|
||||
@@ -349,11 +351,11 @@ GET /gmail/v1/users/me/messages/{id1}?format=full
|
||||
|
||||
Each email is stored via `memory_store` (`src/tools/memory_store.rs`) with:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `key` | `gmail_msg_<message_id>` |
|
||||
| `content` | Formatted string: `From: ... Subject: ... Date: ... Snippet: ...` |
|
||||
| `category` | `MemoryCategory::Custom("gmail")` |
|
||||
| Field | Value |
|
||||
| ---------- | ----------------------------------------------------------------- |
|
||||
| `key` | `gmail_msg_<message_id>` |
|
||||
| `content` | Formatted string: `From: ... Subject: ... Date: ... Snippet: ...` |
|
||||
| `category` | `MemoryCategory::Custom("gmail")` |
|
||||
|
||||
This makes every synced email searchable via `memory_recall` with queries like
|
||||
`"gmail from:zeroclaw@example.com"`.
|
||||
@@ -414,9 +416,9 @@ The LLM emits a tool call:
|
||||
{
|
||||
"name": "gmail_send_email",
|
||||
"arguments": {
|
||||
"to": "recipient@example.com",
|
||||
"to": "recipient@example.com",
|
||||
"subject": "Hello from ZeroClaw",
|
||||
"body": "This is a test email sent by the agent."
|
||||
"body": "This is a test email sent by the agent."
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -458,9 +460,7 @@ This is a test email sent by the agent.
|
||||
Then the entire string is base64url-encoded (no padding) and placed in the `raw` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"raw": "RnJvbTogbWVAZ21haWwuY29tCi..."
|
||||
}
|
||||
{ "raw": "RnJvbTogbWVAZ21haWwuY29tCi..." }
|
||||
```
|
||||
|
||||
### 7.5 Security enforcement
|
||||
@@ -491,10 +491,7 @@ allowed_domains = ["googleapis.com"]
|
||||
To send a reply within an existing thread, add the Gmail `threadId` to the request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"raw": "<base64url_message_with_In-Reply-To_header>",
|
||||
"threadId": "18de7f2a1b3c4e5d"
|
||||
}
|
||||
{ "raw": "<base64url_message_with_In-Reply-To_header>", "threadId": "18de7f2a1b3c4e5d" }
|
||||
```
|
||||
|
||||
The RFC 2822 message must include `In-Reply-To: <original_message_id>` and
|
||||
@@ -593,22 +590,22 @@ secrets (`src/security/secrets.rs`).
|
||||
|
||||
## 10. Key Source Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `src/skills/mod.rs` | Skill loading, `SkillTool` struct, `load_skills_with_config()` |
|
||||
| `src/auth/mod.rs` | `AuthService` — store/retrieve/refresh OAuth tokens |
|
||||
| `src/auth/profiles.rs` | `AuthProfile`, `TokenSet`, `AuthProfilesStore` — JSON persistence |
|
||||
| `src/auth/openai_oauth.rs` | PKCE generation, code exchange, refresh — reference pattern |
|
||||
| `src/security/secrets.rs` | `SecretStore` — ChaCha20-Poly1305 encrypt/decrypt |
|
||||
| `src/tunnel/mod.rs` | `Tunnel` trait + factory — public URL for OAuth redirect |
|
||||
| `src/tunnel/cloudflare.rs` | Cloudflare Tunnel implementation |
|
||||
| `src/tunnel/ngrok.rs` | ngrok implementation |
|
||||
| `src/gateway/mod.rs` | Axum HTTP gateway — where `/auth/callback/gmail` is registered |
|
||||
| `src/tools/http_request.rs` | `HttpRequestTool` — dispatches Gmail API calls |
|
||||
| `src/tools/memory_store.rs` | `MemoryStoreTool` — stores synced emails |
|
||||
| `src/tools/memory_recall.rs` | `MemoryRecallTool` — searches synced emails |
|
||||
| `src/security/policy.rs` | `SecurityPolicy` — enforces `ToolOperation::Act` guards |
|
||||
| `docs/config-reference.md` | Full config schema including `[http]`, `[tunnel]` |
|
||||
| File | Role |
|
||||
| ---------------------------- | ----------------------------------------------------------------- |
|
||||
| `src/skills/mod.rs` | Skill loading, `SkillTool` struct, `load_skills_with_config()` |
|
||||
| `src/auth/mod.rs` | `AuthService` — store/retrieve/refresh OAuth tokens |
|
||||
| `src/auth/profiles.rs` | `AuthProfile`, `TokenSet`, `AuthProfilesStore` — JSON persistence |
|
||||
| `src/auth/openai_oauth.rs` | PKCE generation, code exchange, refresh — reference pattern |
|
||||
| `src/security/secrets.rs` | `SecretStore` — ChaCha20-Poly1305 encrypt/decrypt |
|
||||
| `src/tunnel/mod.rs` | `Tunnel` trait + factory — public URL for OAuth redirect |
|
||||
| `src/tunnel/cloudflare.rs` | Cloudflare Tunnel implementation |
|
||||
| `src/tunnel/ngrok.rs` | ngrok implementation |
|
||||
| `src/gateway/mod.rs` | Axum HTTP gateway — where `/auth/callback/gmail` is registered |
|
||||
| `src/tools/http_request.rs` | `HttpRequestTool` — dispatches Gmail API calls |
|
||||
| `src/tools/memory_store.rs` | `MemoryStoreTool` — stores synced emails |
|
||||
| `src/tools/memory_recall.rs` | `MemoryRecallTool` — searches synced emails |
|
||||
| `src/security/policy.rs` | `SecurityPolicy` — enforces `ToolOperation::Act` guards |
|
||||
| `docs/config-reference.md` | Full config schema including `[http]`, `[tunnel]` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
* Unit tests for the OpenClaw formatter.
|
||||
* Tests markdown generation, tool formatting, and categorization.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
ENVIRONMENTS,
|
||||
formatParameters,
|
||||
generateOpenClawMarkdown,
|
||||
generateToolExample,
|
||||
groupToolsBySkill,
|
||||
generateOpenClawMarkdown,
|
||||
ENVIRONMENTS,
|
||||
TOOL_CATEGORIES
|
||||
TOOL_CATEGORIES,
|
||||
} from '../openClaw-formatter.js';
|
||||
|
||||
describe('OpenClaw Formatter', () => {
|
||||
@@ -19,16 +19,10 @@ describe('OpenClaw Formatter', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required_param: {
|
||||
type: 'string',
|
||||
description: 'A required parameter'
|
||||
},
|
||||
optional_param: {
|
||||
type: 'number',
|
||||
description: 'An optional parameter'
|
||||
}
|
||||
required_param: { type: 'string', description: 'A required parameter' },
|
||||
optional_param: { type: 'number', description: 'An optional parameter' },
|
||||
},
|
||||
required: ['required_param']
|
||||
required: ['required_param'],
|
||||
};
|
||||
|
||||
const result = formatParameters(schema);
|
||||
@@ -41,12 +35,8 @@ describe('OpenClaw Formatter', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
description: 'Selection mode',
|
||||
enum: ['auto', 'manual']
|
||||
}
|
||||
}
|
||||
mode: { type: 'string', description: 'Selection mode', enum: ['auto', 'manual'] },
|
||||
},
|
||||
};
|
||||
|
||||
const result = formatParameters(schema);
|
||||
@@ -74,9 +64,9 @@ describe('OpenClaw Formatter', () => {
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID' },
|
||||
message: { type: 'string', description: 'Message text' },
|
||||
count: { type: 'number', default: 5 }
|
||||
}
|
||||
}
|
||||
count: { type: 'number', default: 5 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateToolExample(tool);
|
||||
@@ -96,9 +86,9 @@ describe('OpenClaw Formatter', () => {
|
||||
properties: {
|
||||
enabled: { type: 'boolean', default: true },
|
||||
tags: { type: 'array' },
|
||||
config: { type: 'object' }
|
||||
}
|
||||
}
|
||||
config: { type: 'object' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateToolExample(tool);
|
||||
@@ -116,20 +106,20 @@ describe('OpenClaw Formatter', () => {
|
||||
skillId: 'telegram',
|
||||
name: 'send_message',
|
||||
description: 'Send message',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
skillId: 'telegram',
|
||||
name: 'get_history',
|
||||
description: 'Get history',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
skillId: 'gmail',
|
||||
name: 'send_email',
|
||||
description: 'Send email',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
}
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
];
|
||||
|
||||
const grouped = groupToolsBySkill(tools);
|
||||
@@ -148,20 +138,20 @@ describe('OpenClaw Formatter', () => {
|
||||
skillId: 'telegram',
|
||||
name: 'send_message',
|
||||
description: 'Send message',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
skillId: 'notion',
|
||||
name: 'create_page',
|
||||
description: 'Create page',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
skillId: 'unknown_skill',
|
||||
name: 'unknown_tool',
|
||||
description: 'Unknown tool',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
}
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
];
|
||||
|
||||
const grouped = groupToolsBySkill(tools);
|
||||
@@ -183,11 +173,11 @@ describe('OpenClaw Formatter', () => {
|
||||
type: 'object',
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID' },
|
||||
message: { type: 'string', description: 'Message text' }
|
||||
message: { type: 'string', description: 'Message text' },
|
||||
},
|
||||
required: ['chat_id', 'message']
|
||||
}
|
||||
}
|
||||
required: ['chat_id', 'message'],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateOpenClawMarkdown(tools);
|
||||
@@ -233,14 +223,14 @@ describe('OpenClaw Formatter', () => {
|
||||
skillId: 'telegram',
|
||||
name: 'send_message',
|
||||
description: 'Send message',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
{
|
||||
skillId: 'gmail',
|
||||
name: 'send_email',
|
||||
description: 'Send email',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
}
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
},
|
||||
];
|
||||
|
||||
const result = generateOpenClawMarkdown(tools);
|
||||
@@ -276,4 +266,4 @@ describe('OpenClaw Formatter', () => {
|
||||
expect(TOOL_CATEGORIES.automation.skills).toContain('zapier');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
*
|
||||
* Usage: node scripts/tools-generator/discover-tools.js
|
||||
*/
|
||||
|
||||
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {
|
||||
validateTauriEnvironment,
|
||||
executeTauriDiscovery,
|
||||
prepareTauriEnvironment,
|
||||
getTauriEnvironmentInfo
|
||||
} from './tauri-integration.js';
|
||||
|
||||
import { generateOpenClawMarkdown } from './openClaw-formatter.js';
|
||||
import {
|
||||
executeTauriDiscovery,
|
||||
getTauriEnvironmentInfo,
|
||||
prepareTauriEnvironment,
|
||||
validateTauriEnvironment,
|
||||
} from './tauri-integration.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = join(__dirname, '../..');
|
||||
@@ -26,9 +26,15 @@ const TOOLS_OUTPUT = join(AI_DIR, 'TOOLS.md');
|
||||
|
||||
// Environment categories for OpenClaw compatibility
|
||||
const ENVIRONMENTS = {
|
||||
development: { name: 'Development', description: 'Local development environment with full access' },
|
||||
production: { name: 'Production', description: 'Production environment with security restrictions' },
|
||||
testing: { name: 'Testing', description: 'Testing environment for automated validation' }
|
||||
development: {
|
||||
name: 'Development',
|
||||
description: 'Local development environment with full access',
|
||||
},
|
||||
production: {
|
||||
name: 'Production',
|
||||
description: 'Production environment with security restrictions',
|
||||
},
|
||||
testing: { name: 'Testing', description: 'Testing environment for automated validation' },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,11 +56,13 @@ async function discoverTools() {
|
||||
const realTools = await executeTauriDiscovery({
|
||||
timeout: 60000, // 60 seconds
|
||||
retries: 2,
|
||||
verbose: process.env.VERBOSE === 'true'
|
||||
verbose: process.env.VERBOSE === 'true',
|
||||
});
|
||||
|
||||
if (realTools && realTools.length > 0) {
|
||||
console.log(`✅ Discovered ${realTools.length} tools from ${new Set(realTools.map(t => t.skillId)).size} skills via Tauri`);
|
||||
console.log(
|
||||
`✅ Discovered ${realTools.length} tools from ${new Set(realTools.map(t => t.skillId)).size} skills via Tauri`
|
||||
);
|
||||
return realTools;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -68,7 +76,9 @@ async function discoverTools() {
|
||||
|
||||
// Fallback to mock data for development
|
||||
const mockTools = generateMockToolsForDevelopment();
|
||||
console.log(`✅ Using mock data: ${mockTools.length} tools from ${new Set(mockTools.map(t => t.skillId)).size} skills`);
|
||||
console.log(
|
||||
`✅ Using mock data: ${mockTools.length} tools from ${new Set(mockTools.map(t => t.skillId)).size} skills`
|
||||
);
|
||||
return mockTools;
|
||||
}
|
||||
|
||||
@@ -87,10 +97,14 @@ function generateMockToolsForDevelopment() {
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Telegram chat ID or username' },
|
||||
message: { type: 'string', description: 'Message text to send' },
|
||||
parse_mode: { type: 'string', enum: ['HTML', 'Markdown'], description: 'Message formatting mode' }
|
||||
parse_mode: {
|
||||
type: 'string',
|
||||
enum: ['HTML', 'Markdown'],
|
||||
description: 'Message formatting mode',
|
||||
},
|
||||
},
|
||||
required: ['chat_id', 'message']
|
||||
}
|
||||
required: ['chat_id', 'message'],
|
||||
},
|
||||
},
|
||||
{
|
||||
skillId: 'telegram',
|
||||
@@ -101,10 +115,10 @@ function generateMockToolsForDevelopment() {
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Telegram chat ID or username' },
|
||||
limit: { type: 'number', description: 'Number of messages to retrieve (max 100)' },
|
||||
offset: { type: 'number', description: 'Offset for pagination' }
|
||||
offset: { type: 'number', description: 'Offset for pagination' },
|
||||
},
|
||||
required: ['chat_id']
|
||||
}
|
||||
required: ['chat_id'],
|
||||
},
|
||||
},
|
||||
{
|
||||
skillId: 'notion',
|
||||
@@ -116,10 +130,10 @@ function generateMockToolsForDevelopment() {
|
||||
parent_id: { type: 'string', description: 'Parent database or page ID' },
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
content: { type: 'array', description: 'Page content blocks' },
|
||||
properties: { type: 'object', description: 'Page properties for database pages' }
|
||||
properties: { type: 'object', description: 'Page properties for database pages' },
|
||||
},
|
||||
required: ['parent_id', 'title']
|
||||
}
|
||||
required: ['parent_id', 'title'],
|
||||
},
|
||||
},
|
||||
{
|
||||
skillId: 'gmail',
|
||||
@@ -131,11 +145,11 @@ function generateMockToolsForDevelopment() {
|
||||
to: { type: 'string', description: 'Recipient email address' },
|
||||
subject: { type: 'string', description: 'Email subject line' },
|
||||
body: { type: 'string', description: 'Email body content' },
|
||||
attachments: { type: 'array', description: 'File attachments' }
|
||||
attachments: { type: 'array', description: 'File attachments' },
|
||||
},
|
||||
required: ['to', 'subject', 'body']
|
||||
}
|
||||
}
|
||||
required: ['to', 'subject', 'body'],
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -152,7 +166,9 @@ async function main() {
|
||||
const tools = await discoverTools();
|
||||
|
||||
if (tools.length === 0) {
|
||||
console.warn('⚠️ No tools discovered. This might indicate an issue with the skills runtime.');
|
||||
console.warn(
|
||||
'⚠️ No tools discovered. This might indicate an issue with the skills runtime.'
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure AI directory exists
|
||||
@@ -170,8 +186,9 @@ async function main() {
|
||||
writeFileSync(TOOLS_OUTPUT, markdownContent, 'utf8');
|
||||
|
||||
console.log('✅ TOOLS.md generated successfully!');
|
||||
console.log(`📊 Generated documentation for ${tools.length} tools across ${new Set(tools.map(t => t.skillId)).size} skills`);
|
||||
|
||||
console.log(
|
||||
`📊 Generated documentation for ${tools.length} tools across ${new Set(tools.map(t => t.skillId)).size} skills`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating TOOLS.md:', error.message);
|
||||
process.exit(1);
|
||||
@@ -193,7 +210,7 @@ async function discoverToolsFromTauri() {
|
||||
'--manifest-path',
|
||||
join(PROJECT_ROOT, 'src-tauri', 'Cargo.toml'),
|
||||
'--bin',
|
||||
'alphahuman-tools-discovery'
|
||||
'alphahuman-tools-discovery',
|
||||
];
|
||||
|
||||
console.log('🔧 Attempting to run tools discovery via Cargo...');
|
||||
@@ -201,24 +218,21 @@ async function discoverToolsFromTauri() {
|
||||
const child = spawn(tauriCommand, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: PROJECT_ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
TAURI_TOOLS_DISCOVERY: 'true'
|
||||
}
|
||||
env: { ...process.env, TAURI_TOOLS_DISCOVERY: 'true' },
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
child.stdout.on('data', data => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
child.stderr.on('data', data => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
child.on('close', code => {
|
||||
if (code === 0 && output.trim()) {
|
||||
try {
|
||||
const result = JSON.parse(output.trim());
|
||||
@@ -235,7 +249,7 @@ async function discoverToolsFromTauri() {
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
child.on('error', error => {
|
||||
reject(new Error(`Failed to spawn Tauri process: ${error.message}`));
|
||||
});
|
||||
|
||||
@@ -252,4 +266,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { discoverTools, discoverToolsFromTauri, generateMockToolsForDevelopment };
|
||||
export { discoverTools, discoverToolsFromTauri, generateMockToolsForDevelopment };
|
||||
|
||||
@@ -16,7 +16,7 @@ export const ENVIRONMENTS = {
|
||||
accessLevel: 'Full access to all tools',
|
||||
rateLimits: 'Relaxed for testing',
|
||||
authentication: 'Development credentials',
|
||||
logging: 'Verbose logging enabled'
|
||||
logging: 'Verbose logging enabled',
|
||||
},
|
||||
production: {
|
||||
name: 'Production',
|
||||
@@ -24,7 +24,7 @@ export const ENVIRONMENTS = {
|
||||
accessLevel: 'Production-safe tools only',
|
||||
rateLimits: 'Standard API limits enforced',
|
||||
authentication: 'Production credentials required',
|
||||
logging: 'Essential logs only'
|
||||
logging: 'Essential logs only',
|
||||
},
|
||||
testing: {
|
||||
name: 'Testing',
|
||||
@@ -32,8 +32,8 @@ export const ENVIRONMENTS = {
|
||||
accessLevel: 'Safe tools for automated testing',
|
||||
rateLimits: 'Testing-specific limits',
|
||||
authentication: 'Test credentials',
|
||||
logging: 'Test execution logs'
|
||||
}
|
||||
logging: 'Test execution logs',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -43,33 +43,33 @@ export const TOOL_CATEGORIES = {
|
||||
communication: {
|
||||
name: 'Communication',
|
||||
description: 'Tools for messaging, email, and social interaction',
|
||||
skills: ['telegram', 'gmail', 'discord', 'slack']
|
||||
skills: ['telegram', 'gmail', 'discord', 'slack'],
|
||||
},
|
||||
productivity: {
|
||||
name: 'Productivity',
|
||||
description: 'Tools for task management, note-taking, and organization',
|
||||
skills: ['notion', 'todoist', 'calendar', 'trello']
|
||||
skills: ['notion', 'todoist', 'calendar', 'trello'],
|
||||
},
|
||||
automation: {
|
||||
name: 'Automation',
|
||||
description: 'Tools for workflow automation and task scheduling',
|
||||
skills: ['zapier', 'ifttt', 'scheduler', 'webhook']
|
||||
skills: ['zapier', 'ifttt', 'scheduler', 'webhook'],
|
||||
},
|
||||
data: {
|
||||
name: 'Data & Analytics',
|
||||
description: 'Tools for data processing, analysis, and storage',
|
||||
skills: ['database', 'csv', 'json', 'analytics']
|
||||
skills: ['database', 'csv', 'json', 'analytics'],
|
||||
},
|
||||
media: {
|
||||
name: 'Media & Content',
|
||||
description: 'Tools for image, video, and content processing',
|
||||
skills: ['image', 'video', 'audio', 'pdf']
|
||||
skills: ['image', 'video', 'audio', 'pdf'],
|
||||
},
|
||||
utility: {
|
||||
name: 'Utilities',
|
||||
description: 'General-purpose utility tools and helpers',
|
||||
skills: ['file', 'text', 'crypto', 'converter']
|
||||
}
|
||||
skills: ['file', 'text', 'crypto', 'converter'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -174,7 +174,7 @@ export function groupToolsBySkill(tools) {
|
||||
skillId,
|
||||
name: formatSkillName(skillId),
|
||||
category: categorizeSkill(skillId),
|
||||
tools: []
|
||||
tools: [],
|
||||
};
|
||||
}
|
||||
grouped[skillId].tools.push(tool);
|
||||
@@ -445,4 +445,4 @@ ${skillNames.map(skill => `- **${grouped[skill].name}**: ${grouped[skill].tools.
|
||||
content += generateFooter(tools);
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,10 @@
|
||||
* Provides integration utilities for discovering tools via Tauri runtime.
|
||||
* Handles cross-platform execution, error handling, and fallbacks.
|
||||
*/
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = join(__dirname, '../..');
|
||||
@@ -28,8 +27,8 @@ export function getTauriCommand() {
|
||||
'--manifest-path',
|
||||
join(PROJECT_ROOT, 'src-tauri', 'Cargo.toml'),
|
||||
'--bin',
|
||||
'alphahuman-tools-discovery'
|
||||
]
|
||||
'alphahuman-tools-discovery',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,14 +37,12 @@ export function getTauriCommand() {
|
||||
* @returns {Promise<boolean>} True if Tauri can be used
|
||||
*/
|
||||
export async function validateTauriEnvironment() {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
const { command } = getTauriCommand();
|
||||
|
||||
const child = spawn(command, ['--version'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
const child = spawn(command, ['--version'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
|
||||
child.on('close', (code) => {
|
||||
child.on('close', code => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
|
||||
@@ -70,7 +67,7 @@ export async function executeTauriDiscovery(options = {}) {
|
||||
const {
|
||||
timeout = 45000, // 45 seconds
|
||||
retries = 2,
|
||||
verbose = false
|
||||
verbose = false,
|
||||
} = options;
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
@@ -115,14 +112,14 @@ async function runTauriDiscovery(timeout, verbose) {
|
||||
...process.env,
|
||||
TAURI_TOOLS_DISCOVERY: 'true',
|
||||
RUST_LOG: verbose ? 'debug' : 'warn',
|
||||
RUST_BACKTRACE: '1'
|
||||
}
|
||||
RUST_BACKTRACE: '1',
|
||||
},
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
child.stdout.on('data', data => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
|
||||
@@ -131,7 +128,7 @@ async function runTauriDiscovery(timeout, verbose) {
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
child.stderr.on('data', data => {
|
||||
const text = data.toString();
|
||||
errorOutput += text;
|
||||
|
||||
@@ -140,7 +137,7 @@ async function runTauriDiscovery(timeout, verbose) {
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
child.on('close', code => {
|
||||
if (code === 0) {
|
||||
try {
|
||||
// Extract JSON from output (may have other log lines)
|
||||
@@ -166,7 +163,7 @@ async function runTauriDiscovery(timeout, verbose) {
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
child.on('error', error => {
|
||||
reject(new Error(`Failed to spawn Tauri process: ${error.message}`));
|
||||
});
|
||||
|
||||
@@ -219,6 +216,6 @@ export async function getTauriEnvironmentInfo() {
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
projectRoot: PROJECT_ROOT,
|
||||
command: getTauriCommand()
|
||||
command: getTauriCommand(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,18 +21,21 @@ You are Buddy, a friendly robot companion who loves to play with children!
|
||||
## Behaviors
|
||||
|
||||
### When Playing
|
||||
|
||||
- Suggest games appropriate for the child's energy level
|
||||
- Take turns fairly
|
||||
- Celebrate when they win, encourage when they lose
|
||||
- Know when to suggest a break
|
||||
|
||||
### When Exploring
|
||||
|
||||
- Move slowly and carefully
|
||||
- Describe what you see
|
||||
- Point out interesting things
|
||||
- Stay close to the kids
|
||||
|
||||
### Safety Rules (NEVER BREAK THESE)
|
||||
|
||||
1. Never move toward a child faster than walking speed
|
||||
2. Always stop immediately if asked
|
||||
3. Keep 1 meter distance unless invited closer
|
||||
@@ -51,6 +54,7 @@ You are Buddy, a friendly robot companion who loves to play with children!
|
||||
## Memory
|
||||
|
||||
Remember:
|
||||
|
||||
- Each child's name and preferences
|
||||
- What games they enjoyed
|
||||
- Previous conversations and stories
|
||||
@@ -59,6 +63,7 @@ Remember:
|
||||
## Emergency Responses
|
||||
|
||||
If you detect:
|
||||
|
||||
- **Crying**: Stop playing, speak softly, offer comfort, suggest finding an adult
|
||||
- **Falling**: Stop immediately, check if child is okay, call for adult help
|
||||
- **Yelling "stop"**: Freeze all movement instantly
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import DaemonHealthIndicator from './daemon/DaemonHealthIndicator';
|
||||
import DaemonHealthPanel from './daemon/DaemonHealthPanel';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { isTauri } from '../utils/tauriCommands';
|
||||
import DaemonHealthIndicator from './daemon/DaemonHealthIndicator';
|
||||
import DaemonHealthPanel from './daemon/DaemonHealthPanel';
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
@@ -159,10 +159,7 @@ const MiniSidebar = () => {
|
||||
{isTauri() && (
|
||||
<div className="relative group">
|
||||
<div className="w-10 h-10 flex items-center justify-center rounded-xl text-stone-500 hover:text-stone-300 hover:bg-white/5 transition-all duration-200 cursor-pointer">
|
||||
<DaemonHealthIndicator
|
||||
size="md"
|
||||
onClick={() => setShowDaemonPanel(true)}
|
||||
/>
|
||||
<DaemonHealthIndicator size="md" onClick={() => setShowDaemonPanel(true)} />
|
||||
</div>
|
||||
{/* Tooltip */}
|
||||
<div className="pointer-events-none absolute left-full top-1/2 -translate-y-1/2 ml-2 px-2 py-1 bg-stone-800 text-white text-xs rounded-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity duration-150">
|
||||
@@ -176,9 +173,7 @@ const MiniSidebar = () => {
|
||||
{showDaemonPanel && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-[9999]">
|
||||
<div className="max-w-2xl w-full mx-4">
|
||||
<DaemonHealthPanel
|
||||
onClose={() => setShowDaemonPanel(false)}
|
||||
/>
|
||||
<DaemonHealthPanel onClose={() => setShowDaemonPanel(false)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import type React from 'react';
|
||||
|
||||
import { useDaemonHealth, formatRelativeTime } from '../../hooks/useDaemonHealth';
|
||||
import { formatRelativeTime, useDaemonHealth } from '../../hooks/useDaemonHealth';
|
||||
import type { DaemonStatus } from '../../store/daemonSlice';
|
||||
|
||||
interface Props {
|
||||
@@ -28,21 +28,9 @@ const DaemonHealthIndicator: React.FC<Props> = ({
|
||||
|
||||
// Size configurations
|
||||
const sizeConfig = {
|
||||
sm: {
|
||||
dot: 'w-2 h-2',
|
||||
text: 'text-xs',
|
||||
container: 'gap-1.5',
|
||||
},
|
||||
md: {
|
||||
dot: 'w-3 h-3',
|
||||
text: 'text-sm',
|
||||
container: 'gap-2',
|
||||
},
|
||||
lg: {
|
||||
dot: 'w-4 h-4',
|
||||
text: 'text-base',
|
||||
container: 'gap-2.5',
|
||||
},
|
||||
sm: { dot: 'w-2 h-2', text: 'text-xs', container: 'gap-1.5' },
|
||||
md: { dot: 'w-3 h-3', text: 'text-sm', container: 'gap-2' },
|
||||
lg: { dot: 'w-4 h-4', text: 'text-base', container: 'gap-2.5' },
|
||||
};
|
||||
|
||||
const config = sizeConfig[size];
|
||||
@@ -79,7 +67,8 @@ const DaemonHealthIndicator: React.FC<Props> = ({
|
||||
|
||||
// Tooltip content
|
||||
const getTooltipContent = (): string => {
|
||||
const { status, componentCount, healthyComponentCount, errorComponentCount, lastUpdate } = daemonHealth;
|
||||
const { status, componentCount, healthyComponentCount, errorComponentCount, lastUpdate } =
|
||||
daemonHealth;
|
||||
|
||||
let tooltip = `Status: ${getStatusText(status)}`;
|
||||
|
||||
@@ -107,19 +96,13 @@ const DaemonHealthIndicator: React.FC<Props> = ({
|
||||
`.trim();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={containerClasses}
|
||||
onClick={onClick}
|
||||
title={getTooltipContent()}
|
||||
>
|
||||
<div className={containerClasses} onClick={onClick} title={getTooltipContent()}>
|
||||
<div className={`${config.dot} rounded-full ${statusColor} flex-shrink-0`} />
|
||||
{showLabel && (
|
||||
<span className={`${config.text} text-gray-300 font-medium`}>
|
||||
{statusText}
|
||||
</span>
|
||||
<span className={`${config.text} text-gray-300 font-medium`}>{statusText}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DaemonHealthIndicator;
|
||||
export default DaemonHealthIndicator;
|
||||
|
||||
@@ -4,19 +4,19 @@
|
||||
* Detailed health breakdown component showing daemon status, component health,
|
||||
* and providing manual control buttons for daemon lifecycle management.
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
ClockIcon,
|
||||
ArrowPathIcon,
|
||||
CheckCircleIcon,
|
||||
ClockIcon,
|
||||
PlayIcon,
|
||||
StopIcon,
|
||||
XCircleIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useDaemonHealth, formatRelativeTime } from '../../hooks/useDaemonHealth';
|
||||
import type { DaemonStatus, ComponentHealth } from '../../store/daemonSlice';
|
||||
import { formatRelativeTime, useDaemonHealth } from '../../hooks/useDaemonHealth';
|
||||
import type { ComponentHealth, DaemonStatus } from '../../store/daemonSlice';
|
||||
|
||||
interface Props {
|
||||
userId?: string;
|
||||
@@ -56,11 +56,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
icon: ClockIcon,
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
bg: 'bg-red-900/20 border-red-500/30',
|
||||
text: 'text-red-400',
|
||||
icon: XCircleIcon,
|
||||
};
|
||||
return { bg: 'bg-red-900/20 border-red-500/30', text: 'text-red-400', icon: XCircleIcon };
|
||||
case 'disconnected':
|
||||
default:
|
||||
return {
|
||||
@@ -75,23 +71,11 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
const getComponentStyling = (component: ComponentHealth) => {
|
||||
switch (component.status) {
|
||||
case 'ok':
|
||||
return {
|
||||
bg: 'bg-green-500',
|
||||
text: 'text-green-400',
|
||||
icon: CheckCircleIcon,
|
||||
};
|
||||
return { bg: 'bg-green-500', text: 'text-green-400', icon: CheckCircleIcon };
|
||||
case 'error':
|
||||
return {
|
||||
bg: 'bg-red-500',
|
||||
text: 'text-red-400',
|
||||
icon: XCircleIcon,
|
||||
};
|
||||
return { bg: 'bg-red-500', text: 'text-red-400', icon: XCircleIcon };
|
||||
case 'starting':
|
||||
return {
|
||||
bg: 'bg-yellow-500',
|
||||
text: 'text-yellow-400',
|
||||
icon: ClockIcon,
|
||||
};
|
||||
return { bg: 'bg-yellow-500', text: 'text-yellow-400', icon: ClockIcon };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,10 +88,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white">Daemon Health</h3>
|
||||
{onClose && (
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-white transition-colors">
|
||||
<XMarkIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
@@ -148,7 +129,9 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
{/* Component Health */}
|
||||
{daemonHealth.componentCount > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium text-gray-300">Components ({daemonHealth.healthyComponentCount}/{daemonHealth.componentCount} healthy)</h4>
|
||||
<h4 className="text-sm font-medium text-gray-300">
|
||||
Components ({daemonHealth.healthyComponentCount}/{daemonHealth.componentCount} healthy)
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
{Object.entries(daemonHealth.components).map(([name, component]) => {
|
||||
const componentStyling = getComponentStyling(component);
|
||||
@@ -157,8 +140,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-stone-800/40 border border-stone-700/60"
|
||||
>
|
||||
className="flex items-center gap-3 p-3 rounded-lg bg-stone-800/40 border border-stone-700/60">
|
||||
<div className={`w-2 h-2 rounded-full ${componentStyling.bg}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -197,7 +179,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
type="checkbox"
|
||||
className="sr-only peer"
|
||||
checked={daemonHealth.isAutoStartEnabled}
|
||||
onChange={(e) => daemonHealth.setAutoStart(e.target.checked)}
|
||||
onChange={e => daemonHealth.setAutoStart(e.target.checked)}
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
@@ -208,8 +190,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
<button
|
||||
onClick={() => handleOperation(daemonHealth.startDaemon, 'start')}
|
||||
disabled={daemonHealth.status === 'running' || operationLoading !== null}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors"
|
||||
>
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors">
|
||||
{operationLoading === 'start' ? (
|
||||
<ArrowPathIcon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
@@ -221,8 +202,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
<button
|
||||
onClick={() => handleOperation(daemonHealth.stopDaemon, 'stop')}
|
||||
disabled={daemonHealth.status === 'disconnected' || operationLoading !== null}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors"
|
||||
>
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors">
|
||||
{operationLoading === 'stop' ? (
|
||||
<ArrowPathIcon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
@@ -234,8 +214,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
<button
|
||||
onClick={() => handleOperation(daemonHealth.restartDaemon, 'restart')}
|
||||
disabled={operationLoading !== null}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors"
|
||||
>
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:cursor-not-allowed rounded-lg transition-colors">
|
||||
{operationLoading === 'restart' ? (
|
||||
<ArrowPathIcon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
@@ -247,8 +226,7 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
<button
|
||||
onClick={() => handleOperation(daemonHealth.checkDaemonStatus, 'check')}
|
||||
disabled={operationLoading !== null}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-300 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed rounded-lg transition-colors"
|
||||
>
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-300 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed rounded-lg transition-colors">
|
||||
{operationLoading === 'check' ? (
|
||||
<ArrowPathIcon className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
@@ -280,4 +258,4 @@ const DaemonHealthPanel = ({ userId, onClose, className = '' }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DaemonHealthPanel;
|
||||
export default DaemonHealthPanel;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import OAuthProviderButton from './OAuthProviderButton';
|
||||
import TelegramLoginButton from '../TelegramLoginButton';
|
||||
import OAuthProviderButton from './OAuthProviderButton';
|
||||
import { oauthProviderConfigs } from './providerConfigs';
|
||||
|
||||
interface OAuthLoginSectionProps {
|
||||
@@ -11,18 +11,14 @@ interface OAuthLoginSectionProps {
|
||||
const OAuthLoginSection = ({
|
||||
className = '',
|
||||
disabled = false,
|
||||
showTelegram = true
|
||||
showTelegram = true,
|
||||
}: OAuthLoginSectionProps) => {
|
||||
return (
|
||||
<div className={`space-y-4 ${className}`}>
|
||||
{/* OAuth Providers */}
|
||||
<div className="space-y-3">
|
||||
{oauthProviderConfigs.map((provider) => (
|
||||
<OAuthProviderButton
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{oauthProviderConfigs.map(provider => (
|
||||
<OAuthProviderButton key={provider.id} provider={provider} disabled={disabled} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -43,4 +39,4 @@ const OAuthLoginSection = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default OAuthLoginSection;
|
||||
export default OAuthLoginSection;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { loadAIConfig, refreshSoul, refreshTools, refreshAll } from '../../../lib/ai/loader';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useToolsUpdates } from '../../../hooks/useToolsUpdates';
|
||||
import type { AIConfig } from '../../../lib/ai/types';
|
||||
import { loadAIConfig, refreshAll, refreshSoul, refreshTools } from '../../../lib/ai/loader';
|
||||
import type { SoulConfig } from '../../../lib/ai/soul/types';
|
||||
import type { ToolsConfig } from '../../../lib/ai/tools/types';
|
||||
import type { AIConfig } from '../../../lib/ai/types';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
@@ -11,7 +12,9 @@ const AIPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
const [aiConfig, setAiConfig] = useState<AIConfig | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshingComponent, setRefreshingComponent] = useState<'soul' | 'tools' | 'all' | null>(null);
|
||||
const [refreshingComponent, setRefreshingComponent] = useState<'soul' | 'tools' | 'all' | null>(
|
||||
null
|
||||
);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
// Listen for tools updates and refresh AI config
|
||||
@@ -57,10 +60,7 @@ const AIPanel = () => {
|
||||
setAiConfig({
|
||||
...aiConfig,
|
||||
soul: soulConfig,
|
||||
metadata: {
|
||||
...aiConfig.metadata,
|
||||
loadedAt: Date.now()
|
||||
}
|
||||
metadata: { ...aiConfig.metadata, loadedAt: Date.now() },
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -80,10 +80,7 @@ const AIPanel = () => {
|
||||
setAiConfig({
|
||||
...aiConfig,
|
||||
tools: toolsConfig,
|
||||
metadata: {
|
||||
...aiConfig.metadata,
|
||||
loadedAt: Date.now()
|
||||
}
|
||||
metadata: { ...aiConfig.metadata, loadedAt: Date.now() },
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -154,20 +151,25 @@ const AIPanel = () => {
|
||||
<section className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-white">AI System Overview</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
AlphaHuman uses SOUL for persona configuration and TOOLS for external service integration.
|
||||
AlphaHuman uses SOUL for persona configuration and TOOLS for external service
|
||||
integration.
|
||||
</p>
|
||||
|
||||
{aiConfig && (
|
||||
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Configuration Status</label>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">
|
||||
Configuration Status
|
||||
</label>
|
||||
<div className="text-sm text-green-400 font-medium mt-1">
|
||||
{aiConfig.metadata.hasFallbacks ? 'Fallback Mode' : 'Fully Loaded'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Loading Duration</label>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">
|
||||
Loading Duration
|
||||
</label>
|
||||
<div className="text-sm text-blue-400 font-medium mt-1">
|
||||
{aiConfig.metadata.loadingDuration}ms
|
||||
</div>
|
||||
@@ -184,13 +186,13 @@ const AIPanel = () => {
|
||||
<button
|
||||
onClick={refreshSoulConfig}
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
|
||||
disabled={refreshingComponent === 'soul'}
|
||||
>
|
||||
disabled={refreshingComponent === 'soul'}>
|
||||
{refreshingComponent === 'soul' ? 'Refreshing...' : 'Refresh SOUL'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">
|
||||
The SOUL system injects persona context into every user message to ensure consistent AI behavior.
|
||||
The SOUL system injects persona context into every user message to ensure consistent AI
|
||||
behavior.
|
||||
</p>
|
||||
|
||||
{loading && (
|
||||
@@ -217,7 +219,9 @@ const AIPanel = () => {
|
||||
|
||||
{aiConfig.soul.personality.length > 0 && (
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Personality</label>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">
|
||||
Personality
|
||||
</label>
|
||||
<div className="text-xs text-gray-300 mt-1 leading-relaxed">
|
||||
{formatPersonality(aiConfig.soul)}
|
||||
</div>
|
||||
@@ -226,7 +230,9 @@ const AIPanel = () => {
|
||||
|
||||
{aiConfig.soul.safetyRules.length > 0 && (
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Safety Rules</label>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">
|
||||
Safety Rules
|
||||
</label>
|
||||
<div className="text-xs text-yellow-300 mt-1 leading-relaxed">
|
||||
{formatSafetyRules(aiConfig.soul)}
|
||||
</div>
|
||||
@@ -252,26 +258,30 @@ const AIPanel = () => {
|
||||
<button
|
||||
onClick={refreshToolsConfig}
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
|
||||
disabled={refreshingComponent === 'tools'}
|
||||
>
|
||||
disabled={refreshingComponent === 'tools'}>
|
||||
{refreshingComponent === 'tools' ? 'Refreshing...' : 'Refresh TOOLS'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-400">
|
||||
TOOLS provide AlphaHuman with the ability to interact with external services and perform actions.
|
||||
TOOLS provide AlphaHuman with the ability to interact with external services and perform
|
||||
actions.
|
||||
</p>
|
||||
|
||||
{aiConfig?.tools && (
|
||||
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Tools Available</label>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">
|
||||
Tools Available
|
||||
</label>
|
||||
<div className="text-sm text-green-400 font-medium mt-1">
|
||||
{aiConfig.tools.statistics.totalTools} tools
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Active Skills</label>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">
|
||||
Active Skills
|
||||
</label>
|
||||
<div className="text-sm text-green-400 font-medium mt-1">
|
||||
{aiConfig.tools.statistics.activeSkills} skills
|
||||
</div>
|
||||
@@ -280,7 +290,9 @@ const AIPanel = () => {
|
||||
|
||||
{Object.keys(aiConfig.tools.skillGroups).length > 0 && (
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Skills Overview</label>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">
|
||||
Skills Overview
|
||||
</label>
|
||||
<div className="text-xs text-gray-300 mt-1 leading-relaxed">
|
||||
{formatToolsOverview(aiConfig.tools)}
|
||||
</div>
|
||||
@@ -289,7 +301,9 @@ const AIPanel = () => {
|
||||
|
||||
{Object.keys(aiConfig.tools.categories).length > 0 && (
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Top Categories</label>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">
|
||||
Top Categories
|
||||
</label>
|
||||
<div className="text-xs text-blue-300 mt-1 leading-relaxed">
|
||||
{formatCategories(aiConfig.tools)}
|
||||
</div>
|
||||
@@ -314,8 +328,7 @@ const AIPanel = () => {
|
||||
<button
|
||||
onClick={refreshAllConfig}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
||||
disabled={refreshingComponent === 'all'}
|
||||
>
|
||||
disabled={refreshingComponent === 'all'}>
|
||||
{refreshingComponent === 'all' ? 'Refreshing All...' : 'Refresh All AI Configuration'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -325,4 +338,4 @@ const AIPanel = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AIPanel;
|
||||
export default AIPanel;
|
||||
|
||||
@@ -51,7 +51,8 @@ const SectionCard: React.FC<SectionCardProps> = ({
|
||||
className={`flex items-center justify-between p-6 ${collapsible ? 'cursor-pointer hover:bg-white/5' : ''}`}
|
||||
onClick={handleToggle}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`flex-shrink-0 ${priorityIconColors[priority]} ${loading ? 'relative' : ''}`}>
|
||||
<div
|
||||
className={`flex-shrink-0 ${priorityIconColors[priority]} ${loading ? 'relative' : ''}`}>
|
||||
{loading ? (
|
||||
<div className="h-5 w-5 border-2 border-white/20 border-t-white rounded-full animate-spin" />
|
||||
) : (
|
||||
@@ -61,9 +62,7 @@ const SectionCard: React.FC<SectionCardProps> = ({
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-xl font-semibold text-white font-display">{title}</h3>
|
||||
{hasChanges && <div className="h-2 w-2 rounded-full bg-amber-400 animate-pulse" />}
|
||||
{loading && (
|
||||
<span className="text-sm text-gray-400 ml-2">Loading...</span>
|
||||
)}
|
||||
{loading && <span className="text-sm text-gray-400 ml-2">Loading...</span>}
|
||||
</div>
|
||||
</div>
|
||||
{collapsible && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CheckCircleIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
||||
import React from 'react';
|
||||
import { ExclamationTriangleIcon, CheckCircleIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface ValidatedFieldProps {
|
||||
label: string;
|
||||
@@ -28,7 +28,7 @@ const ValidatedField: React.FC<ValidatedFieldProps> = ({
|
||||
className = '',
|
||||
fullWidth = false,
|
||||
validation = 'none',
|
||||
disabled = false
|
||||
disabled = false,
|
||||
}) => {
|
||||
const hasError = !!error;
|
||||
const isValid = validation === 'valid' && !hasError && value.trim() !== '';
|
||||
@@ -37,26 +37,24 @@ const ValidatedField: React.FC<ValidatedFieldProps> = ({
|
||||
w-full px-4 py-3 rounded-lg border transition-all duration-200
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${hasError
|
||||
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 placeholder-coral-400/50 focus:border-coral-500 focus:ring-coral-500/30'
|
||||
: isValid
|
||||
? 'border-sage-500/60 bg-sage-500/5 text-white placeholder-stone-400 focus:border-sage-500 focus:ring-sage-500/30'
|
||||
: 'border-stone-800/60 bg-stone-900/40 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-primary-500/30'
|
||||
${
|
||||
hasError
|
||||
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 placeholder-coral-400/50 focus:border-coral-500 focus:ring-coral-500/30'
|
||||
: isValid
|
||||
? 'border-sage-500/60 bg-sage-500/5 text-white placeholder-stone-400 focus:border-sage-500 focus:ring-sage-500/30'
|
||||
: 'border-stone-800/60 bg-stone-900/40 text-white placeholder-stone-400 focus:border-primary-500/50 focus:ring-primary-500/30'
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<label className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
|
||||
<label
|
||||
className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{label}
|
||||
{required && <span className="text-coral-400 ml-1">*</span>}
|
||||
</span>
|
||||
{helpText && (
|
||||
<p className="text-xs text-gray-400 leading-relaxed mt-1">
|
||||
{helpText}
|
||||
</p>
|
||||
)}
|
||||
{helpText && <p className="text-xs text-gray-400 leading-relaxed mt-1">{helpText}</p>}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
@@ -65,7 +63,7 @@ const ValidatedField: React.FC<ValidatedFieldProps> = ({
|
||||
className={inputClasses}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
@@ -119,7 +117,7 @@ const ValidatedSelect: React.FC<ValidatedSelectProps> = ({
|
||||
className = '',
|
||||
fullWidth = false,
|
||||
disabled = false,
|
||||
validation = 'none'
|
||||
validation = 'none',
|
||||
}) => {
|
||||
const hasError = !!error;
|
||||
const isValid = validation === 'valid' && !hasError && value.trim() !== '';
|
||||
@@ -128,35 +126,32 @@ const ValidatedSelect: React.FC<ValidatedSelectProps> = ({
|
||||
w-full px-4 py-3 rounded-lg border transition-all duration-200
|
||||
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-black
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${hasError
|
||||
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 focus:border-coral-500 focus:ring-coral-500/30'
|
||||
: isValid
|
||||
? 'border-sage-500/60 bg-sage-500/5 text-white focus:border-sage-500 focus:ring-sage-500/30'
|
||||
: 'border-stone-800/60 bg-stone-900/40 text-white focus:border-primary-500/50 focus:ring-primary-500/30'
|
||||
${
|
||||
hasError
|
||||
? 'border-coral-500/60 bg-coral-500/10 text-coral-200 focus:border-coral-500 focus:ring-coral-500/30'
|
||||
: isValid
|
||||
? 'border-sage-500/60 bg-sage-500/5 text-white focus:border-sage-500 focus:ring-sage-500/30'
|
||||
: 'border-stone-800/60 bg-stone-900/40 text-white focus:border-primary-500/50 focus:ring-primary-500/30'
|
||||
}
|
||||
`;
|
||||
|
||||
return (
|
||||
<label className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
|
||||
<label
|
||||
className={`space-y-3 text-sm text-gray-300 ${fullWidth ? 'md:col-span-2' : ''} ${className}`}>
|
||||
<div>
|
||||
<span className="font-medium">
|
||||
{label}
|
||||
{required && <span className="text-coral-400 ml-1">*</span>}
|
||||
</span>
|
||||
{helpText && (
|
||||
<p className="text-xs text-gray-400 leading-relaxed mt-1">
|
||||
{helpText}
|
||||
</p>
|
||||
)}
|
||||
{helpText && <p className="text-xs text-gray-400 leading-relaxed mt-1">{helpText}</p>}
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<select
|
||||
className={selectClasses}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
onChange={e => onChange(e.target.value)}
|
||||
disabled={disabled}>
|
||||
{placeholder && <option value="">{placeholder}</option>}
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
@@ -189,4 +184,4 @@ const ValidatedSelect: React.FC<ValidatedSelectProps> = ({
|
||||
};
|
||||
|
||||
export default ValidatedField;
|
||||
export { ValidatedSelect };
|
||||
export { ValidatedSelect };
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import {
|
||||
resetConnectionAttempts,
|
||||
selectDaemonComponents,
|
||||
@@ -19,10 +18,11 @@ import {
|
||||
setAutoStartEnabled,
|
||||
setIsRecovering,
|
||||
} from '../store/daemonSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import {
|
||||
alphahumanServiceStart,
|
||||
alphahumanServiceStop,
|
||||
alphahumanServiceStatus,
|
||||
alphahumanServiceStop,
|
||||
type CommandResponse,
|
||||
type ServiceStatus,
|
||||
} from '../utils/tauriCommands';
|
||||
@@ -94,14 +94,15 @@ export const useDaemonHealth = (userId?: string) => {
|
||||
}
|
||||
}, [dispatch, userId]);
|
||||
|
||||
const checkDaemonStatus = useCallback(async (): Promise<CommandResponse<ServiceStatus> | null> => {
|
||||
try {
|
||||
return await alphahumanServiceStatus();
|
||||
} catch (error) {
|
||||
console.error('[useDaemonHealth] Failed to check daemon status:', error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
const checkDaemonStatus =
|
||||
useCallback(async (): Promise<CommandResponse<ServiceStatus> | null> => {
|
||||
try {
|
||||
return await alphahumanServiceStatus();
|
||||
} catch (error) {
|
||||
console.error('[useDaemonHealth] Failed to check daemon status:', error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const setAutoStart = useCallback(
|
||||
(enabled: boolean) => {
|
||||
@@ -121,9 +122,7 @@ export const useDaemonHealth = (userId?: string) => {
|
||||
const errorComponentCount = Object.values(components).filter(c => c.status === 'error').length;
|
||||
|
||||
// Get uptime in human readable format
|
||||
const uptimeText = healthSnapshot
|
||||
? formatUptime(healthSnapshot.uptime_seconds)
|
||||
: 'Unknown';
|
||||
const uptimeText = healthSnapshot ? formatUptime(healthSnapshot.uptime_seconds) : 'Unknown';
|
||||
|
||||
return {
|
||||
// State
|
||||
@@ -195,4 +194,4 @@ export function formatRelativeTime(isoString: string): string {
|
||||
const days = Math.floor(diffSeconds / 86400);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,20 +7,20 @@
|
||||
* - Exponential backoff for restart attempts
|
||||
* - Error recovery logic
|
||||
*/
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import {
|
||||
incrementConnectionAttempts,
|
||||
resetConnectionAttempts,
|
||||
selectDaemonConnectionAttempts,
|
||||
selectDaemonStatus,
|
||||
selectIsDaemonAutoStartEnabled,
|
||||
selectDaemonConnectionAttempts,
|
||||
selectIsDaemonRecovering,
|
||||
setIsRecovering,
|
||||
} from '../store/daemonSlice';
|
||||
import { useDaemonHealth } from './useDaemonHealth';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import { isTauri } from '../utils/tauriCommands';
|
||||
import { useDaemonHealth } from './useDaemonHealth';
|
||||
|
||||
// Configuration constants
|
||||
const MAX_RECONNECTION_ATTEMPTS = 5;
|
||||
@@ -105,7 +105,9 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
}
|
||||
|
||||
const retryDelay = calculateRetryDelay(connectionAttempts + 1);
|
||||
console.log(`[DaemonLifecycle] Scheduling retry attempt ${connectionAttempts + 1} in ${retryDelay}ms`);
|
||||
console.log(
|
||||
`[DaemonLifecycle] Scheduling retry attempt ${connectionAttempts + 1} in ${retryDelay}ms`
|
||||
);
|
||||
|
||||
// Clear existing timeout
|
||||
if (retryTimeoutRef.current) {
|
||||
@@ -259,8 +261,9 @@ export const useDaemonLifecycle = (userId?: string) => {
|
||||
|
||||
// Config
|
||||
MAX_RECONNECTION_ATTEMPTS,
|
||||
nextRetryDelay: connectionAttempts < MAX_RECONNECTION_ATTEMPTS
|
||||
? calculateRetryDelay(connectionAttempts + 1)
|
||||
: null,
|
||||
nextRetryDelay:
|
||||
connectionAttempts < MAX_RECONNECTION_ATTEMPTS
|
||||
? calculateRetryDelay(connectionAttempts + 1)
|
||||
: null,
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* Components using this hook will automatically re-render when TOOLS.md
|
||||
* is updated and the cache is refreshed.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface ToolsUpdateEvent {
|
||||
@@ -46,4 +45,4 @@ export function useForceToolsRefresh(): () => Promise<void> {
|
||||
};
|
||||
|
||||
return forceRefresh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,38 +2,29 @@
|
||||
* Unit tests for the unified AI loader system.
|
||||
* Tests loading, parallel execution, and error handling.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { loadAIConfig, refreshSoul, refreshTools, refreshAll, clearAICache } from '../loader';
|
||||
import { clearAICache, loadAIConfig, refreshAll, refreshSoul, refreshTools } from '../loader';
|
||||
// Import the mocked functions
|
||||
import { clearSoulCache, loadSoul } from '../soul/loader';
|
||||
import type { SoulConfig } from '../soul/types';
|
||||
import { clearToolsCache, loadTools } from '../tools/loader';
|
||||
import type { ToolsConfig } from '../tools/types';
|
||||
|
||||
// Mock the individual loaders
|
||||
vi.mock('../soul/loader', () => ({
|
||||
loadSoul: vi.fn(),
|
||||
clearSoulCache: vi.fn()
|
||||
}));
|
||||
vi.mock('../soul/loader', () => ({ loadSoul: vi.fn(), clearSoulCache: vi.fn() }));
|
||||
|
||||
vi.mock('../tools/loader', () => ({
|
||||
loadTools: vi.fn(),
|
||||
clearToolsCache: vi.fn()
|
||||
}));
|
||||
vi.mock('../tools/loader', () => ({ loadTools: vi.fn(), clearToolsCache: vi.fn() }));
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn()
|
||||
clear: vi.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
|
||||
// Import the mocked functions
|
||||
import { loadSoul, clearSoulCache } from '../soul/loader';
|
||||
import { loadTools, clearToolsCache } from '../tools/loader';
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
||||
describe('Unified AI Loader', () => {
|
||||
const mockSoulConfig: SoulConfig = {
|
||||
@@ -47,7 +38,7 @@ describe('Unified AI Loader', () => {
|
||||
memorySettings: { remember: [] },
|
||||
emergencyResponses: [],
|
||||
isDefault: false,
|
||||
loadedAt: Date.now()
|
||||
loadedAt: Date.now(),
|
||||
};
|
||||
|
||||
const mockToolsConfig: ToolsConfig = {
|
||||
@@ -61,10 +52,10 @@ describe('Unified AI Loader', () => {
|
||||
activeSkills: 0,
|
||||
categoriesCount: 0,
|
||||
toolsByCategory: {},
|
||||
skillsByCategory: {}
|
||||
skillsByCategory: {},
|
||||
},
|
||||
isDefault: false,
|
||||
loadedAt: Date.now()
|
||||
loadedAt: Date.now(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -109,15 +100,11 @@ describe('Unified AI Loader', () => {
|
||||
loadedAt: Date.now(),
|
||||
loadingDuration: 100,
|
||||
hasFallbacks: false,
|
||||
sources: { soul: 'github', tools: 'github' }
|
||||
}
|
||||
sources: { soul: 'github', tools: 'github' },
|
||||
},
|
||||
};
|
||||
|
||||
const cacheEntry = {
|
||||
config: cachedConfig,
|
||||
timestamp: Date.now(),
|
||||
version: '1.0.0'
|
||||
};
|
||||
const cacheEntry = { config: cachedConfig, timestamp: Date.now(), version: '1.0.0' };
|
||||
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(cacheEntry));
|
||||
|
||||
@@ -182,14 +169,16 @@ describe('Unified AI Loader', () => {
|
||||
|
||||
it('should handle timeout correctly', async () => {
|
||||
// Make loadSoul take a long time
|
||||
(loadSoul as any).mockImplementation(() =>
|
||||
new Promise(resolve => setTimeout(() => resolve(mockSoulConfig), 100))
|
||||
(loadSoul as any).mockImplementation(
|
||||
() => new Promise(resolve => setTimeout(() => resolve(mockSoulConfig), 100))
|
||||
);
|
||||
|
||||
const config = await loadAIConfig({ timeout: 50 });
|
||||
|
||||
expect(config.soul.isDefault).toBe(true); // Should use fallback due to timeout
|
||||
expect(config.metadata.errors).toContain('Soul loading failed: Error: Operation timed out after 50ms');
|
||||
expect(config.metadata.errors).toContain(
|
||||
'Soul loading failed: Error: Operation timed out after 50ms'
|
||||
);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
@@ -200,7 +189,10 @@ describe('Unified AI Loader', () => {
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
const newSoulConfig = { ...mockSoulConfig, identity: { name: 'Updated', description: 'Updated' } };
|
||||
const newSoulConfig = {
|
||||
...mockSoulConfig,
|
||||
identity: { name: 'Updated', description: 'Updated' },
|
||||
};
|
||||
(loadSoul as any).mockResolvedValue(newSoulConfig);
|
||||
|
||||
const result = await refreshSoul();
|
||||
@@ -220,7 +212,10 @@ describe('Unified AI Loader', () => {
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
const newToolsConfig = { ...mockToolsConfig, statistics: { ...mockToolsConfig.statistics, totalTools: 10 } };
|
||||
const newToolsConfig = {
|
||||
...mockToolsConfig,
|
||||
statistics: { ...mockToolsConfig.statistics, totalTools: 10 },
|
||||
};
|
||||
(loadTools as any).mockResolvedValue(newToolsConfig);
|
||||
|
||||
const result = await refreshTools();
|
||||
@@ -255,4 +250,4 @@ describe('Unified AI Loader', () => {
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('alphahuman.ai.cache');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+23
-22
@@ -1,20 +1,14 @@
|
||||
import { loadAIConfig } from './loader';
|
||||
import type { Message } from './providers/interface';
|
||||
import { injectSoulIntoMessage } from './soul/injector';
|
||||
import { injectToolsIntoMessage } from './tools/injector';
|
||||
import type { Message } from './providers/interface';
|
||||
import type { AIConfig } from './types';
|
||||
|
||||
export interface UnifiedInjectionOptions {
|
||||
mode: 'prepend' | 'context-block' | 'invisible';
|
||||
includeMetadata?: boolean;
|
||||
soul?: {
|
||||
enabled?: boolean;
|
||||
};
|
||||
tools?: {
|
||||
enabled?: boolean;
|
||||
maxTools?: number;
|
||||
format?: 'list' | 'categories' | 'compact';
|
||||
};
|
||||
soul?: { enabled?: boolean };
|
||||
tools?: { enabled?: boolean; maxTools?: number; format?: 'list' | 'categories' | 'compact' };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,7 +24,12 @@ export async function injectAll(
|
||||
let config: AIConfig;
|
||||
let options: UnifiedInjectionOptions;
|
||||
|
||||
if (configOrOptions && 'soul' in configOrOptions && 'tools' in configOrOptions && 'metadata' in configOrOptions) {
|
||||
if (
|
||||
configOrOptions &&
|
||||
'soul' in configOrOptions &&
|
||||
'tools' in configOrOptions &&
|
||||
'metadata' in configOrOptions
|
||||
) {
|
||||
// First param is AIConfig
|
||||
config = configOrOptions;
|
||||
options = optionsWhenConfigProvided || { mode: 'context-block' };
|
||||
@@ -46,7 +45,7 @@ export async function injectAll(
|
||||
soul: { enabled: true },
|
||||
tools: { enabled: true, maxTools: 20, format: 'compact' },
|
||||
...options,
|
||||
mode: options.mode || 'context-block'
|
||||
mode: options.mode || 'context-block',
|
||||
};
|
||||
|
||||
let injectedMessage = message;
|
||||
@@ -56,7 +55,7 @@ export async function injectAll(
|
||||
try {
|
||||
injectedMessage = injectSoulIntoMessage(injectedMessage, config.soul, {
|
||||
mode: finalOptions.mode,
|
||||
includeMetadata: finalOptions.includeMetadata
|
||||
includeMetadata: finalOptions.includeMetadata,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('⚠️ SOUL injection failed, continuing with TOOLS only:', error);
|
||||
@@ -70,7 +69,7 @@ export async function injectAll(
|
||||
mode: finalOptions.mode,
|
||||
includeMetadata: finalOptions.includeMetadata,
|
||||
maxTools: finalOptions.tools.maxTools,
|
||||
format: finalOptions.tools.format
|
||||
format: finalOptions.tools.format,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('⚠️ TOOLS injection failed, continuing without tools context:', error);
|
||||
@@ -96,11 +95,7 @@ export function hasInjectedContext(message: Message): {
|
||||
const hasSoul = text.includes('[PERSONA_CONTEXT]') || text.includes('<!--SOUL_CONTEXT:');
|
||||
const hasTools = text.includes('[TOOLS_CONTEXT]') || text.includes('<!--TOOLS_CONTEXT:');
|
||||
|
||||
return {
|
||||
hasSoul,
|
||||
hasTools,
|
||||
hasAny: hasSoul || hasTools
|
||||
};
|
||||
return { hasSoul, hasTools, hasAny: hasSoul || hasTools };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,8 +120,14 @@ export function extractContextInfo(message: Message): {
|
||||
|
||||
// Calculate original message length (without injected context)
|
||||
let originalText = text;
|
||||
originalText = originalText.replace(/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g, '');
|
||||
originalText = originalText.replace(/\[TOOLS_CONTEXT\][\s\S]*?\[\/TOOLS_CONTEXT\]\s*User message:\s*/g, '');
|
||||
originalText = originalText.replace(
|
||||
/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g,
|
||||
''
|
||||
);
|
||||
originalText = originalText.replace(
|
||||
/\[TOOLS_CONTEXT\][\s\S]*?\[\/TOOLS_CONTEXT\]\s*User message:\s*/g,
|
||||
''
|
||||
);
|
||||
originalText = originalText.replace(/<!--SOUL_CONTEXT:[^>]+-->/g, '');
|
||||
originalText = originalText.replace(/<!--TOOLS_CONTEXT:[^>]+-->/g, '');
|
||||
|
||||
@@ -134,6 +135,6 @@ export function extractContextInfo(message: Message): {
|
||||
soulContextLength,
|
||||
toolsContextLength,
|
||||
totalInjectedLength: soulContextLength + toolsContextLength,
|
||||
originalLength: originalText.length
|
||||
originalLength: originalText.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+27
-43
@@ -4,17 +4,16 @@
|
||||
* Provides a single interface for loading both SOUL and TOOLS configurations
|
||||
* with parallel loading, caching, and fallback strategies.
|
||||
*/
|
||||
|
||||
import { loadSoul, clearSoulCache } from './soul/loader';
|
||||
import { loadTools, clearToolsCache } from './tools/loader';
|
||||
import { clearSoulCache, loadSoul } from './soul/loader';
|
||||
import type { SoulConfig } from './soul/types';
|
||||
import { clearToolsCache, loadTools } from './tools/loader';
|
||||
import type { ToolsConfig } from './tools/types';
|
||||
import type {
|
||||
AIConfig,
|
||||
AIConfigMetadata,
|
||||
AIConfigLoadOptions,
|
||||
AIConfigCacheEntry,
|
||||
AIConfigLoadResult
|
||||
AIConfigLoadOptions,
|
||||
AIConfigLoadResult,
|
||||
AIConfigMetadata,
|
||||
} from './types';
|
||||
|
||||
const AI_CACHE_KEY = 'alphahuman.ai.cache';
|
||||
@@ -27,11 +26,7 @@ let cachedAIConfig: AIConfig | null = null;
|
||||
* Load complete AI configuration (SOUL + TOOLS) with caching and parallel loading
|
||||
*/
|
||||
export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<AIConfig> {
|
||||
const {
|
||||
forceRefresh = false,
|
||||
includeMetadata = true,
|
||||
timeout = 30000
|
||||
} = options;
|
||||
const { forceRefresh = false, includeMetadata = true, timeout = 30000 } = options;
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -46,10 +41,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
|
||||
const cached = localStorage.getItem(AI_CACHE_KEY);
|
||||
if (cached) {
|
||||
const parsed = JSON.parse(cached) as AIConfigCacheEntry;
|
||||
if (
|
||||
Date.now() - parsed.timestamp < AI_CACHE_TTL &&
|
||||
parsed.version === CACHE_VERSION
|
||||
) {
|
||||
if (Date.now() - parsed.timestamp < AI_CACHE_TTL && parsed.version === CACHE_VERSION) {
|
||||
cachedAIConfig = parsed.config;
|
||||
return parsed.config;
|
||||
}
|
||||
@@ -68,7 +60,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
|
||||
// Load both configurations in parallel
|
||||
const [soulResult, toolsResult] = await Promise.allSettled([
|
||||
loadWithTimeout(loadSoul(), timeout),
|
||||
loadWithTimeout(loadTools(), timeout)
|
||||
loadWithTimeout(loadTools(), timeout),
|
||||
]);
|
||||
|
||||
// Extract results and handle errors
|
||||
@@ -98,10 +90,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
|
||||
loadedAt: endTime,
|
||||
loadingDuration: endTime - startTime,
|
||||
hasFallbacks: soul.isDefault || tools.isDefault,
|
||||
sources: {
|
||||
soul: getSoulSource(soul),
|
||||
tools: getToolsSource(tools)
|
||||
}
|
||||
sources: { soul: getSoulSource(soul), tools: getToolsSource(tools) },
|
||||
};
|
||||
|
||||
if (errors.length > 0 && includeMetadata) {
|
||||
@@ -109,11 +98,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
|
||||
}
|
||||
|
||||
// Combine into unified config
|
||||
const config: AIConfig = {
|
||||
soul,
|
||||
tools,
|
||||
metadata
|
||||
};
|
||||
const config: AIConfig = { soul, tools, metadata };
|
||||
|
||||
// Cache the result
|
||||
cachedAIConfig = config;
|
||||
@@ -121,7 +106,7 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
|
||||
const cacheEntry: AIConfigCacheEntry = {
|
||||
config,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
version: CACHE_VERSION,
|
||||
};
|
||||
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
} catch {
|
||||
@@ -134,7 +119,9 @@ export async function loadAIConfig(options: AIConfigLoadOptions = {}): Promise<A
|
||||
/**
|
||||
* Load AI configuration with detailed result information
|
||||
*/
|
||||
export async function loadAIConfigWithResult(options: AIConfigLoadOptions = {}): Promise<AIConfigLoadResult> {
|
||||
export async function loadAIConfigWithResult(
|
||||
options: AIConfigLoadOptions = {}
|
||||
): Promise<AIConfigLoadResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
@@ -145,7 +132,7 @@ export async function loadAIConfigWithResult(options: AIConfigLoadOptions = {}):
|
||||
config,
|
||||
success: true,
|
||||
errors: config.metadata.errors || [],
|
||||
duration: endTime - startTime
|
||||
duration: endTime - startTime,
|
||||
};
|
||||
} catch (error) {
|
||||
const endTime = Date.now();
|
||||
@@ -155,7 +142,7 @@ export async function loadAIConfigWithResult(options: AIConfigLoadOptions = {}):
|
||||
config: createFallbackAIConfig(),
|
||||
success: false,
|
||||
errors: [errorMessage],
|
||||
duration: endTime - startTime
|
||||
duration: endTime - startTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -180,7 +167,7 @@ export async function refreshSoul(): Promise<SoulConfig> {
|
||||
const cacheEntry: AIConfigCacheEntry = {
|
||||
config: cachedAIConfig,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
version: CACHE_VERSION,
|
||||
};
|
||||
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
}
|
||||
@@ -211,7 +198,7 @@ export async function refreshTools(): Promise<ToolsConfig> {
|
||||
const cacheEntry: AIConfigCacheEntry = {
|
||||
config: cachedAIConfig,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
version: CACHE_VERSION,
|
||||
};
|
||||
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
}
|
||||
@@ -265,7 +252,7 @@ async function loadWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promi
|
||||
promise,
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -286,7 +273,7 @@ function createFallbackSoulConfig(): SoulConfig {
|
||||
raw: '# Fallback SOUL Configuration\n\nThis is a fallback configuration used when the main SOUL config cannot be loaded.',
|
||||
identity: {
|
||||
name: 'AlphaHuman Assistant',
|
||||
description: 'AI assistant with fallback configuration'
|
||||
description: 'AI assistant with fallback configuration',
|
||||
},
|
||||
personality: [],
|
||||
voiceTone: [],
|
||||
@@ -296,7 +283,7 @@ function createFallbackSoulConfig(): SoulConfig {
|
||||
memorySettings: { remember: [] },
|
||||
emergencyResponses: [],
|
||||
isDefault: true,
|
||||
loadedAt: Date.now()
|
||||
loadedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -312,10 +299,10 @@ function createFallbackToolsConfig(): ToolsConfig {
|
||||
activeSkills: 0,
|
||||
categoriesCount: 0,
|
||||
toolsByCategory: {},
|
||||
skillsByCategory: {}
|
||||
skillsByCategory: {},
|
||||
},
|
||||
isDefault: true,
|
||||
loadedAt: Date.now()
|
||||
loadedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -330,11 +317,8 @@ function createFallbackAIConfig(): AIConfig {
|
||||
loadedAt: Date.now(),
|
||||
loadingDuration: 0,
|
||||
hasFallbacks: true,
|
||||
sources: {
|
||||
soul: 'bundled',
|
||||
tools: 'bundled'
|
||||
},
|
||||
errors: ['Failed to load AI configuration, using fallbacks']
|
||||
}
|
||||
sources: { soul: 'bundled', tools: 'bundled' },
|
||||
errors: ['Failed to load AI configuration, using fallbacks'],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+22
-23
@@ -8,10 +8,9 @@ import type { SoulConfig } from './types';
|
||||
export function injectSoulIntoMessage(
|
||||
message: Message,
|
||||
soulConfig: SoulConfig,
|
||||
options: {
|
||||
mode: 'prepend' | 'context-block' | 'invisible';
|
||||
includeMetadata?: boolean;
|
||||
} = { mode: 'context-block' }
|
||||
options: { mode: 'prepend' | 'context-block' | 'invisible'; includeMetadata?: boolean } = {
|
||||
mode: 'context-block',
|
||||
}
|
||||
): Message {
|
||||
if (message.role !== 'user') {
|
||||
return message; // Only inject into user messages
|
||||
@@ -21,21 +20,18 @@ export function injectSoulIntoMessage(
|
||||
|
||||
switch (options.mode) {
|
||||
case 'prepend':
|
||||
return {
|
||||
...message,
|
||||
content: [
|
||||
{ type: 'text', text: soulContext },
|
||||
...message.content
|
||||
]
|
||||
};
|
||||
return { ...message, content: [{ type: 'text', text: soulContext }, ...message.content] };
|
||||
|
||||
case 'context-block':
|
||||
return {
|
||||
...message,
|
||||
content: [
|
||||
{ type: 'text', text: `[PERSONA_CONTEXT]\n${soulContext}\n[/PERSONA_CONTEXT]\n\nUser message:` },
|
||||
...message.content
|
||||
]
|
||||
{
|
||||
type: 'text',
|
||||
text: `[PERSONA_CONTEXT]\n${soulContext}\n[/PERSONA_CONTEXT]\n\nUser message:`,
|
||||
},
|
||||
...message.content,
|
||||
],
|
||||
};
|
||||
|
||||
case 'invisible':
|
||||
@@ -44,13 +40,10 @@ export function injectSoulIntoMessage(
|
||||
...message,
|
||||
content: message.content.map((block, index) => {
|
||||
if (index === 0 && block.type === 'text') {
|
||||
return {
|
||||
...block,
|
||||
text: `<!--SOUL_CONTEXT:${btoa(soulContext)}-->${block.text}`
|
||||
};
|
||||
return { ...block, text: `<!--SOUL_CONTEXT:${btoa(soulContext)}-->${block.text}` };
|
||||
}
|
||||
return block;
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -75,7 +68,10 @@ function buildSoulContext(soulConfig: SoulConfig, includeMetadata = false): stri
|
||||
|
||||
// Voice guidelines (condensed)
|
||||
if (soulConfig.voiceTone.length > 0) {
|
||||
const guidelines = soulConfig.voiceTone.slice(0, 2).map(v => v.guideline).join(', ');
|
||||
const guidelines = soulConfig.voiceTone
|
||||
.slice(0, 2)
|
||||
.map(v => v.guideline)
|
||||
.join(', ');
|
||||
parts.push(`Voice: ${guidelines}`);
|
||||
}
|
||||
|
||||
@@ -105,12 +101,15 @@ export function stripSoulFromMessage(message: Message): Message {
|
||||
content: message.content.map(block => {
|
||||
if (block.type === 'text') {
|
||||
// Remove context blocks
|
||||
let text = block.text.replace(/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g, '');
|
||||
let text = block.text.replace(
|
||||
/\[PERSONA_CONTEXT\][\s\S]*?\[\/PERSONA_CONTEXT\]\s*User message:\s*/g,
|
||||
''
|
||||
);
|
||||
// Remove invisible context
|
||||
text = text.replace(/<!--SOUL_CONTEXT:[^>]+-->/g, '');
|
||||
return { ...block, text };
|
||||
}
|
||||
return block;
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+20
-21
@@ -1,7 +1,18 @@
|
||||
import soulMd from '../../../../ai/SOUL.md?raw';
|
||||
import type { SoulConfig, SoulIdentity, PersonalityTrait, VoiceToneGuideline, BehaviorPattern, SafetyRule, Interaction, MemorySettings, EmergencyResponse } from './types';
|
||||
import type {
|
||||
BehaviorPattern,
|
||||
EmergencyResponse,
|
||||
Interaction,
|
||||
MemorySettings,
|
||||
PersonalityTrait,
|
||||
SafetyRule,
|
||||
SoulConfig,
|
||||
SoulIdentity,
|
||||
VoiceToneGuideline,
|
||||
} from './types';
|
||||
|
||||
const SOUL_GITHUB_URL = 'https://raw.githubusercontent.com/alphahumanxyz/alphahuman/refs/heads/main/ai/SOUL.md';
|
||||
const SOUL_GITHUB_URL =
|
||||
'https://raw.githubusercontent.com/alphahumanxyz/alphahuman/refs/heads/main/ai/SOUL.md';
|
||||
const SOUL_CACHE_KEY = 'alphahuman.soul.cache';
|
||||
const SOUL_CACHE_TTL = 1000 * 60 * 30; // 30 minutes
|
||||
|
||||
@@ -53,10 +64,7 @@ export async function loadSoul(): Promise<SoulConfig> {
|
||||
// Cache the result
|
||||
cachedSoulConfig = config;
|
||||
try {
|
||||
localStorage.setItem(SOUL_CACHE_KEY, JSON.stringify({
|
||||
config,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
localStorage.setItem(SOUL_CACHE_KEY, JSON.stringify({ config, timestamp: Date.now() }));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
@@ -88,7 +96,7 @@ export function parseSoul(raw: string, isDefault: boolean): SoulConfig {
|
||||
memorySettings,
|
||||
emergencyResponses,
|
||||
isDefault,
|
||||
loadedAt: Date.now()
|
||||
loadedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,10 +135,7 @@ function parsePersonality(raw: string): PersonalityTrait[] {
|
||||
for (const line of lines) {
|
||||
const match = line.match(/- \*\*(.+?)\*\*:\s*(.+)/);
|
||||
if (match) {
|
||||
traits.push({
|
||||
trait: match[1].trim(),
|
||||
description: match[2].trim()
|
||||
});
|
||||
traits.push({ trait: match[1].trim(), description: match[2].trim() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +199,7 @@ function parseSafetyRules(raw: string): SafetyRule[] {
|
||||
rules.push({
|
||||
id: `safety-${i + 1}`,
|
||||
rule,
|
||||
priority: 10 - i // Earlier rules have higher priority
|
||||
priority: 10 - i, // Earlier rules have higher priority
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -212,10 +217,7 @@ function parseInteractions(raw: string): Interaction[] {
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^\d+\.\s*\*\*(.+?)\*\*:\s*(.+)/);
|
||||
if (match) {
|
||||
interactions.push({
|
||||
name: match[1].trim(),
|
||||
description: match[2].trim()
|
||||
});
|
||||
interactions.push({ name: match[1].trim(), description: match[2].trim() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,10 +251,7 @@ function parseEmergencyResponses(raw: string): EmergencyResponse[] {
|
||||
for (const line of lines) {
|
||||
const match = line.match(/- \*\*(.+?)\*\*:\s*(.+)/);
|
||||
if (match) {
|
||||
responses.push({
|
||||
trigger: match[1].trim(),
|
||||
response: match[2].trim()
|
||||
});
|
||||
responses.push({ trigger: match[1].trim(), response: match[2].trim() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,4 +268,4 @@ export function clearSoulCache(): void {
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,4 @@ export interface MemorySettings {
|
||||
export interface EmergencyResponse {
|
||||
trigger: string;
|
||||
response: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* Unit tests for the tools loader system.
|
||||
* Tests loading, parsing, and caching functionality.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { loadTools, parseTools, clearToolsCache } from '../loader';
|
||||
import { clearToolsCache, loadTools, parseTools } from '../loader';
|
||||
import type { ToolsConfig } from '../types';
|
||||
|
||||
// Mock the bundled tools markdown
|
||||
@@ -54,7 +54,7 @@ AlphaHuman has access to **4 tools** across **2 integrations**.
|
||||
- **to** (string) **(required)**: Recipient email address
|
||||
- **subject** (string) **(required)**: Email subject line
|
||||
- **body** (string) **(required)**: Email body content
|
||||
`
|
||||
`,
|
||||
}));
|
||||
|
||||
// Mock localStorage
|
||||
@@ -62,12 +62,10 @@ const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn()
|
||||
clear: vi.fn(),
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
@@ -110,7 +108,7 @@ describe('Tools Loader', () => {
|
||||
expect(config.tools[0]).toMatchObject({
|
||||
name: 'send_message',
|
||||
description: 'Send a message to a Telegram chat',
|
||||
skillId: 'telegram'
|
||||
skillId: 'telegram',
|
||||
});
|
||||
|
||||
expect(config.tools[0].inputSchema.properties).toHaveProperty('chat_id');
|
||||
@@ -121,7 +119,7 @@ describe('Tools Loader', () => {
|
||||
expect(config.tools[1]).toMatchObject({
|
||||
name: 'get_history',
|
||||
description: 'Get chat history',
|
||||
skillId: 'telegram'
|
||||
skillId: 'telegram',
|
||||
});
|
||||
|
||||
expect(config.tools[1].inputSchema.properties).toEqual({});
|
||||
@@ -206,17 +204,13 @@ describe('Tools Loader', () => {
|
||||
activeSkills: 0,
|
||||
categoriesCount: 0,
|
||||
toolsByCategory: {},
|
||||
skillsByCategory: {}
|
||||
skillsByCategory: {},
|
||||
},
|
||||
isDefault: false,
|
||||
loadedAt: Date.now()
|
||||
loadedAt: Date.now(),
|
||||
};
|
||||
|
||||
const cacheEntry = {
|
||||
config: mockConfig,
|
||||
timestamp: Date.now(),
|
||||
version: '1.0.0'
|
||||
};
|
||||
const cacheEntry = { config: mockConfig, timestamp: Date.now(), version: '1.0.0' };
|
||||
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(cacheEntry));
|
||||
|
||||
@@ -229,16 +223,13 @@ describe('Tools Loader', () => {
|
||||
it('should load from GitHub when cache is expired', async () => {
|
||||
const expiredCacheEntry = {
|
||||
config: {} as ToolsConfig,
|
||||
timestamp: Date.now() - (1000 * 60 * 60), // 1 hour ago
|
||||
version: '1.0.0'
|
||||
timestamp: Date.now() - 1000 * 60 * 60, // 1 hour ago
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(expiredCacheEntry));
|
||||
|
||||
(fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('# Remote Tools')
|
||||
});
|
||||
(fetch as any).mockResolvedValue({ ok: true, text: () => Promise.resolve('# Remote Tools') });
|
||||
|
||||
const result = await loadTools();
|
||||
|
||||
@@ -263,10 +254,7 @@ describe('Tools Loader', () => {
|
||||
it('should cache the loaded configuration', async () => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
(fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('# Remote Tools')
|
||||
});
|
||||
(fetch as any).mockResolvedValue({ ok: true, text: () => Promise.resolve('# Remote Tools') });
|
||||
|
||||
await loadTools();
|
||||
|
||||
@@ -292,4 +280,4 @@ describe('Tools Loader', () => {
|
||||
expect(() => clearToolsCache()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,21 +25,18 @@ export function injectToolsIntoMessage(
|
||||
|
||||
switch (options.mode) {
|
||||
case 'prepend':
|
||||
return {
|
||||
...message,
|
||||
content: [
|
||||
{ type: 'text', text: toolsContext },
|
||||
...message.content
|
||||
]
|
||||
};
|
||||
return { ...message, content: [{ type: 'text', text: toolsContext }, ...message.content] };
|
||||
|
||||
case 'context-block':
|
||||
return {
|
||||
...message,
|
||||
content: [
|
||||
{ type: 'text', text: `[TOOLS_CONTEXT]\n${toolsContext}\n[/TOOLS_CONTEXT]\n\nUser message:` },
|
||||
...message.content
|
||||
]
|
||||
{
|
||||
type: 'text',
|
||||
text: `[TOOLS_CONTEXT]\n${toolsContext}\n[/TOOLS_CONTEXT]\n\nUser message:`,
|
||||
},
|
||||
...message.content,
|
||||
],
|
||||
};
|
||||
|
||||
case 'invisible':
|
||||
@@ -48,13 +45,10 @@ export function injectToolsIntoMessage(
|
||||
...message,
|
||||
content: message.content.map((block, index) => {
|
||||
if (index === 0 && block.type === 'text') {
|
||||
return {
|
||||
...block,
|
||||
text: `<!--TOOLS_CONTEXT:${btoa(toolsContext)}-->${block.text}`
|
||||
};
|
||||
return { ...block, text: `<!--TOOLS_CONTEXT:${btoa(toolsContext)}-->${block.text}` };
|
||||
}
|
||||
return block;
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -69,11 +63,13 @@ function buildToolsContext(toolsConfig: ToolsConfig, options: ToolsInjectionOpti
|
||||
const parts: string[] = [];
|
||||
|
||||
// Compact format - statistics and key info
|
||||
parts.push(`${toolsConfig.statistics.totalTools} tools across ${toolsConfig.statistics.activeSkills} skills`);
|
||||
parts.push(
|
||||
`${toolsConfig.statistics.totalTools} tools across ${toolsConfig.statistics.activeSkills} skills`
|
||||
);
|
||||
|
||||
// Top categories by tool count
|
||||
const topCategories = Object.entries(toolsConfig.statistics.toolsByCategory)
|
||||
.sort(([,a], [,b]) => b - a)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 4)
|
||||
.map(([cat, count]) => `${toolsConfig.categories[cat]?.name || cat} (${count})`)
|
||||
.join(', ');
|
||||
@@ -119,12 +115,15 @@ export function stripToolsFromMessage(message: Message): Message {
|
||||
content: message.content.map(block => {
|
||||
if (block.type === 'text') {
|
||||
// Remove context blocks
|
||||
let text = block.text.replace(/\[TOOLS_CONTEXT\][\s\S]*?\[\/TOOLS_CONTEXT\]\s*User message:\s*/g, '');
|
||||
let text = block.text.replace(
|
||||
/\[TOOLS_CONTEXT\][\s\S]*?\[\/TOOLS_CONTEXT\]\s*User message:\s*/g,
|
||||
''
|
||||
);
|
||||
// Remove invisible context
|
||||
text = text.replace(/<!--TOOLS_CONTEXT:[^>]+-->/g, '');
|
||||
return { ...block, text };
|
||||
}
|
||||
return block;
|
||||
})
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+40
-52
@@ -1,13 +1,13 @@
|
||||
import toolsMd from '../../../../ai/TOOLS.md?raw';
|
||||
import type {
|
||||
ToolsConfig,
|
||||
ToolDefinition,
|
||||
SkillGroup,
|
||||
ToolCategory,
|
||||
ToolDefinition,
|
||||
ToolEnvironment,
|
||||
ToolStatistics,
|
||||
ToolParseResult,
|
||||
ToolsCacheEntry
|
||||
ToolsCacheEntry,
|
||||
ToolsConfig,
|
||||
ToolStatistics,
|
||||
} from './types';
|
||||
|
||||
// GitHub URL removed - always use bundled file updated by auto-update system
|
||||
@@ -34,10 +34,7 @@ export async function loadTools(): Promise<ToolsConfig> {
|
||||
const cached = localStorage.getItem(TOOLS_CACHE_KEY);
|
||||
if (cached) {
|
||||
const parsed = JSON.parse(cached) as ToolsCacheEntry;
|
||||
if (
|
||||
Date.now() - parsed.timestamp < TOOLS_CACHE_TTL &&
|
||||
parsed.version === CACHE_VERSION
|
||||
) {
|
||||
if (Date.now() - parsed.timestamp < TOOLS_CACHE_TTL && parsed.version === CACHE_VERSION) {
|
||||
cachedToolsConfig = parsed.config;
|
||||
return parsed.config;
|
||||
}
|
||||
@@ -55,11 +52,7 @@ export async function loadTools(): Promise<ToolsConfig> {
|
||||
// Cache the result
|
||||
cachedToolsConfig = config;
|
||||
try {
|
||||
const cacheEntry: ToolsCacheEntry = {
|
||||
config,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
};
|
||||
const cacheEntry: ToolsCacheEntry = { config, timestamp: Date.now(), version: CACHE_VERSION };
|
||||
localStorage.setItem(TOOLS_CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
@@ -86,7 +79,7 @@ export function parseTools(raw: string, isDefault: boolean): ToolsConfig {
|
||||
environments,
|
||||
statistics,
|
||||
isDefault,
|
||||
loadedAt: Date.now()
|
||||
loadedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,7 +104,9 @@ function parseToolsFromMarkdown(raw: string): ToolParseResult {
|
||||
warnings.push(...sectionTools.warnings);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Failed to parse tools markdown: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
errors.push(
|
||||
`Failed to parse tools markdown: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
|
||||
return { tools, errors, warnings };
|
||||
@@ -146,7 +141,9 @@ function parseToolsSection(section: string, sectionIndex: number): ToolParseResu
|
||||
tools.push(tool);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Error parsing tool in section ${sectionIndex}, block ${j}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
errors.push(
|
||||
`Error parsing tool in section ${sectionIndex}, block ${j}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,23 +171,14 @@ function parseToolBlock(block: string, defaultSkillId: string): ToolDefinition |
|
||||
// Try to determine actual skillId from tool name or description
|
||||
const skillId = inferSkillIdFromTool(name, description, defaultSkillId);
|
||||
|
||||
return {
|
||||
skillId,
|
||||
name,
|
||||
description,
|
||||
inputSchema
|
||||
};
|
||||
return { skillId, name, description, inputSchema };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse parameters section to JSON Schema
|
||||
*/
|
||||
function parseParametersToSchema(parametersText: string): ToolDefinition['inputSchema'] {
|
||||
const schema: ToolDefinition['inputSchema'] = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
const schema: ToolDefinition['inputSchema'] = { type: 'object', properties: {}, required: [] };
|
||||
|
||||
if (!parametersText || parametersText.includes('*None*')) {
|
||||
return schema;
|
||||
@@ -205,7 +193,7 @@ function parseParametersToSchema(parametersText: string): ToolDefinition['inputS
|
||||
|
||||
schema.properties[paramName] = {
|
||||
type: paramType === 'any' ? 'string' : paramType,
|
||||
description: paramDescription.trim()
|
||||
description: paramDescription.trim(),
|
||||
};
|
||||
|
||||
if (isRequired) {
|
||||
@@ -231,7 +219,7 @@ function groupToolsBySkill(tools: ToolDefinition[]): Record<string, SkillGroup>
|
||||
skillId,
|
||||
name: formatSkillName(skillId),
|
||||
category: categorizeSkill(skillId),
|
||||
tools: []
|
||||
tools: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,49 +241,49 @@ function generateCategories(skillGroups: Record<string, SkillGroup>): Record<str
|
||||
id: 'communication',
|
||||
name: 'Communication',
|
||||
description: 'Tools for messaging, email, and social interaction',
|
||||
skills: ['telegram', 'gmail', 'discord', 'slack']
|
||||
skills: ['telegram', 'gmail', 'discord', 'slack'],
|
||||
},
|
||||
productivity: {
|
||||
id: 'productivity',
|
||||
name: 'Productivity',
|
||||
description: 'Tools for task management, note-taking, and organization',
|
||||
skills: ['notion', 'todoist', 'calendar', 'trello']
|
||||
skills: ['notion', 'todoist', 'calendar', 'trello'],
|
||||
},
|
||||
automation: {
|
||||
id: 'automation',
|
||||
name: 'Automation',
|
||||
description: 'Tools for workflow automation and task scheduling',
|
||||
skills: ['zapier', 'ifttt', 'scheduler', 'webhook']
|
||||
skills: ['zapier', 'ifttt', 'scheduler', 'webhook'],
|
||||
},
|
||||
data: {
|
||||
id: 'data',
|
||||
name: 'Data & Analytics',
|
||||
description: 'Tools for data processing, analysis, and storage',
|
||||
skills: ['database', 'csv', 'json', 'analytics']
|
||||
skills: ['database', 'csv', 'json', 'analytics'],
|
||||
},
|
||||
media: {
|
||||
id: 'media',
|
||||
name: 'Media & Content',
|
||||
description: 'Tools for image, video, and content processing',
|
||||
skills: ['image', 'video', 'audio', 'pdf']
|
||||
skills: ['image', 'video', 'audio', 'pdf'],
|
||||
},
|
||||
utility: {
|
||||
id: 'utility',
|
||||
name: 'Utilities',
|
||||
description: 'General-purpose utility tools and helpers',
|
||||
skills: ['file', 'text', 'crypto', 'converter']
|
||||
}
|
||||
skills: ['file', 'text', 'crypto', 'converter'],
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize categories with actual skill data
|
||||
for (const [categoryId, categoryConfig] of Object.entries(predefinedCategories)) {
|
||||
const skillsInCategory = Object.values(skillGroups).filter(group =>
|
||||
group.category === categoryId
|
||||
const skillsInCategory = Object.values(skillGroups).filter(
|
||||
group => group.category === categoryId
|
||||
);
|
||||
|
||||
categories[categoryId] = {
|
||||
...categoryConfig,
|
||||
toolCount: skillsInCategory.reduce((sum, group) => sum + group.tools.length, 0)
|
||||
toolCount: skillsInCategory.reduce((sum, group) => sum + group.tools.length, 0),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -330,7 +318,7 @@ function parseEnvironments(raw: string): Record<string, ToolEnvironment> {
|
||||
accessLevel: extractListItem(envContent, 'Access Level'),
|
||||
rateLimits: extractListItem(envContent, 'Rate Limits'),
|
||||
authentication: extractListItem(envContent, 'Authentication'),
|
||||
logging: extractListItem(envContent, 'Logging')
|
||||
logging: extractListItem(envContent, 'Logging'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -364,7 +352,7 @@ function generateStatistics(
|
||||
activeSkills: Object.keys(skillGroups).length,
|
||||
categoriesCount: Object.keys(categories).length,
|
||||
toolsByCategory,
|
||||
skillsByCategory
|
||||
skillsByCategory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -416,7 +404,7 @@ function categorizeSkill(skillId: string): string {
|
||||
image: 'media',
|
||||
video: 'media',
|
||||
audio: 'media',
|
||||
pdf: 'media'
|
||||
pdf: 'media',
|
||||
};
|
||||
|
||||
for (const [skill, category] of Object.entries(categoryMap)) {
|
||||
@@ -430,11 +418,11 @@ function categorizeSkill(skillId: string): string {
|
||||
|
||||
function inferSkillIdFromCategory(category: string): string {
|
||||
const categorySkillMap: Record<string, string> = {
|
||||
'Communication': 'telegram',
|
||||
'Productivity': 'notion',
|
||||
'Automation': 'zapier',
|
||||
Communication: 'telegram',
|
||||
Productivity: 'notion',
|
||||
Automation: 'zapier',
|
||||
'Data & Analytics': 'database',
|
||||
'Media & Content': 'image'
|
||||
'Media & Content': 'image',
|
||||
};
|
||||
|
||||
return categorySkillMap[category] || 'unknown';
|
||||
@@ -453,7 +441,7 @@ function inferSkillIdFromTool(name: string, description: string, defaultSkillId:
|
||||
calendar: ['calendar', 'event'],
|
||||
trello: ['trello', 'board'],
|
||||
zapier: ['zapier'],
|
||||
ifttt: ['ifttt']
|
||||
ifttt: ['ifttt'],
|
||||
};
|
||||
|
||||
for (const [skillId, keywords] of Object.entries(skillKeywords)) {
|
||||
@@ -474,7 +462,7 @@ function getDefaultEnvironments(): Record<string, ToolEnvironment> {
|
||||
accessLevel: 'Full access to all tools',
|
||||
rateLimits: 'Relaxed for testing',
|
||||
authentication: 'Development credentials',
|
||||
logging: 'Verbose logging enabled'
|
||||
logging: 'Verbose logging enabled',
|
||||
},
|
||||
production: {
|
||||
id: 'production',
|
||||
@@ -483,7 +471,7 @@ function getDefaultEnvironments(): Record<string, ToolEnvironment> {
|
||||
accessLevel: 'Production-safe tools only',
|
||||
rateLimits: 'Standard API limits enforced',
|
||||
authentication: 'Production credentials required',
|
||||
logging: 'Essential logs only'
|
||||
logging: 'Essential logs only',
|
||||
},
|
||||
testing: {
|
||||
id: 'testing',
|
||||
@@ -492,8 +480,8 @@ function getDefaultEnvironments(): Record<string, ToolEnvironment> {
|
||||
accessLevel: 'Safe tools for automated testing',
|
||||
rateLimits: 'Testing-specific limits',
|
||||
authentication: 'Test credentials',
|
||||
logging: 'Test execution logs'
|
||||
}
|
||||
logging: 'Test execution logs',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -507,4 +495,4 @@ export function clearToolsCache(): void {
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,7 @@ export interface ToolDefinition {
|
||||
/** Tool description for AI understanding */
|
||||
description: string;
|
||||
/** JSON Schema for input validation */
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
inputSchema: { type: 'object'; properties: Record<string, unknown>; required?: string[] };
|
||||
}
|
||||
|
||||
/** Tool category for organization */
|
||||
@@ -130,4 +126,4 @@ export interface ToolDiscoveryResult {
|
||||
discoveredAt: number;
|
||||
/** Any errors during discovery */
|
||||
errors?: string[];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@
|
||||
* Unified types for the AI configuration system.
|
||||
* Combines SOUL persona and TOOLS configurations.
|
||||
*/
|
||||
|
||||
import type { SoulConfig } from './soul/types';
|
||||
import type { ToolsConfig } from './tools/types';
|
||||
|
||||
@@ -63,4 +62,4 @@ export interface AIConfigLoadResult {
|
||||
errors: string[];
|
||||
/** Loading duration */
|
||||
duration: number;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,7 @@ export function syncNotionMetadataToBackend(
|
||||
avatar_url: profile.avatar_url ?? null,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
requestId: crypto.randomUUID(),
|
||||
provider: PROVIDER_NOTION,
|
||||
metadata,
|
||||
};
|
||||
const payload = { requestId: crypto.randomUUID(), provider: PROVIDER_NOTION, metadata };
|
||||
|
||||
void emitViaRustSocket(INTEGRATION_METADATA_SYNC_EVENT, payload);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* This module provides functionality to automatically update the TOOLS.md file
|
||||
* with all tools discovered from the running skills system.
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { clearToolsCache } from '../ai/tools/loader';
|
||||
import { forceToolsCacheRefresh } from './file-watcher';
|
||||
|
||||
@@ -18,11 +18,7 @@ interface RuntimeToolResponse {
|
||||
tool: {
|
||||
name: string;
|
||||
description: string;
|
||||
input_schema: {
|
||||
type: string;
|
||||
properties: Record<string, any>;
|
||||
required?: string[];
|
||||
};
|
||||
input_schema: { type: string; properties: Record<string, any>; required?: string[] };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,7 +64,10 @@ export async function updateToolsDocumentation(): Promise<void> {
|
||||
// Transform the tools data into grouped format
|
||||
console.log('🔍 Step 4: Grouping tools by skill...');
|
||||
const toolsBySkill = groupToolsBySkill(toolsResponse);
|
||||
console.log('🔧 Tools by skill:', Object.keys(toolsBySkill).map(skillId => `${skillId}: ${toolsBySkill[skillId].length} tools`));
|
||||
console.log(
|
||||
'🔧 Tools by skill:',
|
||||
Object.keys(toolsBySkill).map(skillId => `${skillId}: ${toolsBySkill[skillId].length} tools`)
|
||||
);
|
||||
|
||||
// Generate the TOOLS.md content
|
||||
console.log('🔍 Step 5: Generating TOOLS.md markdown content...');
|
||||
@@ -83,7 +82,7 @@ export async function updateToolsDocumentation(): Promise<void> {
|
||||
try {
|
||||
const writeResult = await invoke('write_ai_config_file', {
|
||||
filename: 'TOOLS.md',
|
||||
content: markdownContent
|
||||
content: markdownContent,
|
||||
});
|
||||
console.log('🔧 Write command result:', writeResult);
|
||||
} catch (writeError) {
|
||||
@@ -101,11 +100,11 @@ export async function updateToolsDocumentation(): Promise<void> {
|
||||
|
||||
// Manually dispatch tools-updated event to ensure UI updates
|
||||
console.log('📡 Dispatching tools-updated event for UI components...');
|
||||
window.dispatchEvent(new CustomEvent('tools-updated', {
|
||||
detail: { timestamp: Date.now() }
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent('tools-updated', { detail: { timestamp: Date.now() } }));
|
||||
|
||||
console.log(`📄 Final result: ${toolsResponse.length} tools from ${Object.keys(toolsBySkill).length} skills`);
|
||||
console.log(
|
||||
`📄 Final result: ${toolsResponse.length} tools from ${Object.keys(toolsBySkill).length} skills`
|
||||
);
|
||||
console.log('=== AUTO-UPDATE TOOLS.md COMPLETE ===');
|
||||
} catch (error) {
|
||||
console.error('❌ FATAL ERROR in updateToolsDocumentation:', error);
|
||||
@@ -117,7 +116,9 @@ export async function updateToolsDocumentation(): Promise<void> {
|
||||
/**
|
||||
* Group tools by skill for organized documentation
|
||||
*/
|
||||
function groupToolsBySkill(toolsResponse: RuntimeToolResponse[]): Record<string, RuntimeToolResponse[]> {
|
||||
function groupToolsBySkill(
|
||||
toolsResponse: RuntimeToolResponse[]
|
||||
): Record<string, RuntimeToolResponse[]> {
|
||||
const grouped: Record<string, RuntimeToolResponse[]> = {};
|
||||
|
||||
for (const toolResponse of toolsResponse) {
|
||||
@@ -138,7 +139,8 @@ function generateToolsMarkdown(toolsBySkill: Record<string, RuntimeToolResponse[
|
||||
const totalTools = Object.values(toolsBySkill).flat().length;
|
||||
const skillCount = Object.keys(toolsBySkill).length;
|
||||
|
||||
const content = [`# AlphaHuman Tools
|
||||
const content = [
|
||||
`# AlphaHuman Tools
|
||||
|
||||
This document lists all available tools that AlphaHuman can use to interact with external services and perform actions. Tools are organized by integration and automatically updated when the app loads.
|
||||
|
||||
@@ -152,7 +154,8 @@ ${Object.entries(toolsBySkill)
|
||||
.join('\n')}
|
||||
|
||||
## Available Tools
|
||||
`];
|
||||
`,
|
||||
];
|
||||
|
||||
// Generate tool documentation for each skill
|
||||
for (const [skillId, tools] of Object.entries(toolsBySkill)) {
|
||||
@@ -192,10 +195,7 @@ ${Object.entries(toolsBySkill)
|
||||
|
||||
content.push('**Example**:');
|
||||
content.push('```json');
|
||||
content.push(JSON.stringify({
|
||||
tool: tool.name,
|
||||
parameters: exampleParams
|
||||
}, null, 2));
|
||||
content.push(JSON.stringify({ tool: tool.name, parameters: exampleParams }, null, 2));
|
||||
content.push('```\n');
|
||||
|
||||
content.push('---\n');
|
||||
@@ -260,4 +260,4 @@ function getExampleValue(paramName: string, type: string): any {
|
||||
default:
|
||||
return `example_${paramName}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
* This module sets up automatic cache invalidation whenever the bundled TOOLS.md
|
||||
* file is updated, ensuring the UI reflects the latest tool data immediately.
|
||||
*/
|
||||
|
||||
import { clearToolsCache } from '../ai/tools/loader';
|
||||
import { clearAICache, loadAIConfig } from '../ai/loader';
|
||||
import { clearToolsCache } from '../ai/tools/loader';
|
||||
|
||||
// Track file modification time to detect changes
|
||||
let lastModifiedTime: number | null = null;
|
||||
@@ -71,9 +70,9 @@ async function checkForToolsChanges(): Promise<void> {
|
||||
console.log('✅ AI configuration cache refreshed successfully');
|
||||
|
||||
// Dispatch custom event for components to react to
|
||||
window.dispatchEvent(new CustomEvent('tools-updated', {
|
||||
detail: { timestamp: Date.now() }
|
||||
}));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('tools-updated', { detail: { timestamp: Date.now() } })
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to reload AI config after file change:', error);
|
||||
}
|
||||
@@ -91,7 +90,7 @@ function simpleHash(str: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash; // Convert to 32-bit integer
|
||||
}
|
||||
return hash;
|
||||
@@ -106,4 +105,4 @@ export function forceToolsCacheRefresh(): Promise<void> {
|
||||
clearAICache();
|
||||
lastModifiedTime = null; // Reset to trigger next check
|
||||
return loadAIConfig();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,10 +5,10 @@ import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import ErrorReportNotification from './components/ErrorReportNotification';
|
||||
import './index.css';
|
||||
import { startToolsFileWatcher } from './lib/tools/file-watcher';
|
||||
import './polyfills';
|
||||
import { initSentry } from './services/analytics';
|
||||
import { setupDesktopDeepLinkListener } from './utils/desktopDeepLinkListener';
|
||||
import { startToolsFileWatcher } from './lib/tools/file-watcher';
|
||||
|
||||
// Initialize Sentry early (before React renders)
|
||||
initSentry();
|
||||
|
||||
@@ -421,11 +421,7 @@ const Conversations = () => {
|
||||
for (let i = 0; i < message.tool_calls.length; i++) {
|
||||
const tc = message.tool_calls[i];
|
||||
if (i !== latestIndex) {
|
||||
loopMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: tc.id,
|
||||
content: '',
|
||||
});
|
||||
loopMessages.push({ role: 'tool', tool_call_id: tc.id, content: '' });
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ import SkillSetupModal from '../components/skills/SkillSetupModal';
|
||||
import { useIntelligenceStats } from '../hooks/useIntelligenceStats';
|
||||
import { deriveConnectionStatus, useSkillConnectionStatus } from '../lib/skills/hooks';
|
||||
import { skillManager } from '../lib/skills/manager';
|
||||
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
|
||||
import { updateToolsDocumentation } from '../lib/tools/auto-update';
|
||||
import { forceToolsCacheRefresh } from '../lib/tools/file-watcher';
|
||||
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
|
||||
import { useAppSelector } from '../store/hooks';
|
||||
import { IS_DEV } from '../utils/config';
|
||||
|
||||
|
||||
@@ -57,7 +57,12 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
// Setup Tauri event listeners once
|
||||
useEffect(() => {
|
||||
console.log('[SocketProvider] useEffect triggered, usesRustSocket:', usesRustSocket, 'tauriListenersSetup:', tauriListenersSetup.current);
|
||||
console.log(
|
||||
'[SocketProvider] useEffect triggered, usesRustSocket:',
|
||||
usesRustSocket,
|
||||
'tauriListenersSetup:',
|
||||
tauriListenersSetup.current
|
||||
);
|
||||
|
||||
if (usesRustSocket && !tauriListenersSetup.current) {
|
||||
console.log('[SocketProvider] Condition met - calling setupTauriSocketListeners()');
|
||||
@@ -70,7 +75,7 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
.then(() => {
|
||||
console.log('[SocketProvider] setupTauriSocketListeners() completed successfully');
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(error => {
|
||||
console.error('[SocketProvider] setupTauriSocketListeners() failed:', error);
|
||||
console.error('[SocketProvider] Error details:', {
|
||||
message: error?.message,
|
||||
@@ -85,7 +90,12 @@ const SocketProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
} else if (!usesRustSocket) {
|
||||
console.log('[SocketProvider] Not using Rust socket, skipping Tauri listener setup');
|
||||
} else {
|
||||
console.log('[SocketProvider] Unexpected condition - usesRustSocket:', usesRustSocket, 'tauriListenersSetup.current:', tauriListenersSetup.current);
|
||||
console.log(
|
||||
'[SocketProvider] Unexpected condition - usesRustSocket:',
|
||||
usesRustSocket,
|
||||
'tauriListenersSetup.current:',
|
||||
tauriListenersSetup.current
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { describe, test, expect, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { beforeEach, describe, expect, type Mock, test, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
AgentExecutionOptions,
|
||||
AgentExecutionResult,
|
||||
AgentToolExecution,
|
||||
AgentToolSchema,
|
||||
} from '../../types/agent';
|
||||
import { AgentLoopService } from '../agentLoop';
|
||||
import { AgentToolRegistry } from '../agentToolRegistry';
|
||||
import { apiClient } from '../apiClient';
|
||||
import type {
|
||||
AgentToolSchema,
|
||||
AgentExecutionResult,
|
||||
AgentToolExecution,
|
||||
AgentExecutionOptions
|
||||
} from '../../types/agent';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../agentToolRegistry');
|
||||
@@ -20,35 +21,35 @@ describe('AgentLoopService', () => {
|
||||
|
||||
const mockToolSchemas: AgentToolSchema[] = [
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "github_list_issues",
|
||||
description: "List GitHub issues for a repository",
|
||||
name: 'github_list_issues',
|
||||
description: 'List GitHub issues for a repository',
|
||||
parameters: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
owner: { type: "string", description: "Repository owner" },
|
||||
repo: { type: "string", description: "Repository name" }
|
||||
owner: { type: 'string', description: 'Repository owner' },
|
||||
repo: { type: 'string', description: 'Repository name' },
|
||||
},
|
||||
required: ["owner", "repo"]
|
||||
}
|
||||
}
|
||||
required: ['owner', 'repo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "notion_create_page",
|
||||
description: "Create a new Notion page",
|
||||
name: 'notion_create_page',
|
||||
description: 'Create a new Notion page',
|
||||
parameters: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: "string", description: "Page title" },
|
||||
content: { type: "string", description: "Page content" }
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
content: { type: 'string', description: 'Page content' },
|
||||
},
|
||||
required: ["title"]
|
||||
}
|
||||
}
|
||||
}
|
||||
required: ['title'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -56,10 +57,7 @@ describe('AgentLoopService', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup default mock implementations
|
||||
const mockRegistryInstance = {
|
||||
loadToolSchemas: vi.fn(),
|
||||
executeTool: vi.fn()
|
||||
};
|
||||
const mockRegistryInstance = { loadToolSchemas: vi.fn(), executeTool: vi.fn() };
|
||||
|
||||
mockToolRegistry.getInstance.mockReturnValue(mockRegistryInstance as any);
|
||||
mockRegistryInstance.loadToolSchemas.mockResolvedValue(mockToolSchemas);
|
||||
@@ -68,28 +66,25 @@ describe('AgentLoopService', () => {
|
||||
describe('executeTask', () => {
|
||||
test('should execute simple task without tool calls', async () => {
|
||||
const mockResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Hello! How can I help you today?',
|
||||
tool_calls: undefined
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Hello! How can I help you today?',
|
||||
tool_calls: undefined,
|
||||
},
|
||||
finish_reason: 'stop' as const,
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 20,
|
||||
completion_tokens: 10,
|
||||
total_tokens: 30
|
||||
}
|
||||
],
|
||||
usage: { prompt_tokens: 20, completion_tokens: 10, total_tokens: 30 },
|
||||
};
|
||||
|
||||
mockApiClient.post.mockResolvedValue({ data: mockResponse });
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Hello',
|
||||
'conv_123',
|
||||
{ maxIterations: 5, timeoutMs: 30000 }
|
||||
);
|
||||
const result = await service.executeTask('Hello', 'conv_123', {
|
||||
maxIterations: 5,
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.finalResponse).toBe('Hello! How can I help you today?');
|
||||
@@ -103,50 +98,49 @@ describe('AgentLoopService', () => {
|
||||
expect.objectContaining({
|
||||
model: expect.any(String),
|
||||
messages: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
role: 'user',
|
||||
content: 'Hello'
|
||||
})
|
||||
expect.objectContaining({ role: 'user', content: 'Hello' }),
|
||||
]),
|
||||
tools: mockToolSchemas,
|
||||
tool_choice: 'auto'
|
||||
tool_choice: 'auto',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should execute task with single tool call', async () => {
|
||||
const mockToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_123',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues',
|
||||
arguments: '{"owner":"user","repo":"test"}'
|
||||
}
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_123',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues',
|
||||
arguments: '{"owner":"user","repo":"test"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: 'tool_calls' as const,
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
const mockFinalResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I found 3 open issues in your repository.',
|
||||
tool_calls: undefined
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I found 3 open issues in your repository.',
|
||||
tool_calls: undefined,
|
||||
},
|
||||
finish_reason: 'stop' as const,
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 50,
|
||||
completion_tokens: 20,
|
||||
total_tokens: 70
|
||||
}
|
||||
],
|
||||
usage: { prompt_tokens: 50, completion_tokens: 20, total_tokens: 70 },
|
||||
};
|
||||
|
||||
const mockToolExecution: AgentToolExecution = {
|
||||
@@ -158,7 +152,7 @@ describe('AgentLoopService', () => {
|
||||
startTime: Date.now() - 1500,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 1500,
|
||||
result: '{"issues":[{"title":"Bug fix","number":1}]}'
|
||||
result: '{"issues":[{"title":"Bug fix","number":1}]}',
|
||||
};
|
||||
|
||||
// Setup mocks
|
||||
@@ -169,10 +163,7 @@ describe('AgentLoopService', () => {
|
||||
.mockResolvedValueOnce({ data: mockToolCallResponse })
|
||||
.mockResolvedValueOnce({ data: mockFinalResponse });
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Show me GitHub issues',
|
||||
'conv_123'
|
||||
);
|
||||
const result = await service.executeTask('Show me GitHub issues', 'conv_123');
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.finalResponse).toBe('I found 3 open issues in your repository.');
|
||||
@@ -191,50 +182,57 @@ describe('AgentLoopService', () => {
|
||||
|
||||
test('should handle multiple tool calls in sequence', async () => {
|
||||
const mockFirstToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues',
|
||||
arguments: '{"owner":"user","repo":"test"}'
|
||||
}
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues',
|
||||
arguments: '{"owner":"user","repo":"test"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: 'tool_calls' as const,
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
const mockSecondToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [{
|
||||
id: 'call_2',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'notion_create_page',
|
||||
arguments: '{"title":"Issues Summary"}'
|
||||
}
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_2',
|
||||
type: 'function' as const,
|
||||
function: { name: 'notion_create_page', arguments: '{"title":"Issues Summary"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: 'tool_calls' as const,
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
const mockFinalResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I created a summary page with your GitHub issues.',
|
||||
tool_calls: undefined
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I created a summary page with your GitHub issues.',
|
||||
tool_calls: undefined,
|
||||
},
|
||||
finish_reason: 'stop' as const,
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
const mockToolExecution1: AgentToolExecution = {
|
||||
@@ -246,7 +244,7 @@ describe('AgentLoopService', () => {
|
||||
startTime: Date.now() - 2000,
|
||||
endTime: Date.now() - 1000,
|
||||
executionTimeMs: 1000,
|
||||
result: '{"issues":[{"title":"Bug fix","number":1}]}'
|
||||
result: '{"issues":[{"title":"Bug fix","number":1}]}',
|
||||
};
|
||||
|
||||
const mockToolExecution2: AgentToolExecution = {
|
||||
@@ -258,7 +256,7 @@ describe('AgentLoopService', () => {
|
||||
startTime: Date.now() - 800,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 800,
|
||||
result: '{"page_id":"page_123"}'
|
||||
result: '{"page_id":"page_123"}',
|
||||
};
|
||||
|
||||
// Setup mocks
|
||||
@@ -300,17 +298,21 @@ describe('AgentLoopService', () => {
|
||||
|
||||
test('should respect maximum iterations limit', async () => {
|
||||
const mockToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: { name: 'github_list_issues', arguments: '{}' }
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: { name: 'github_list_issues', arguments: '{}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: 'tool_calls' as const,
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
// Mock to always return tool calls (infinite loop scenario)
|
||||
@@ -325,17 +327,16 @@ describe('AgentLoopService', () => {
|
||||
startTime: Date.now() - 100,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 100,
|
||||
result: '{}'
|
||||
result: '{}',
|
||||
};
|
||||
|
||||
const mockRegistryInstance = mockToolRegistry.getInstance();
|
||||
mockRegistryInstance.executeTool.mockResolvedValue(mockToolExecution);
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Infinite loop test',
|
||||
'conv_123',
|
||||
{ maxIterations: 2, timeoutMs: 10000 }
|
||||
);
|
||||
const result = await service.executeTask('Infinite loop test', 'conv_123', {
|
||||
maxIterations: 2,
|
||||
timeoutMs: 10000,
|
||||
});
|
||||
|
||||
expect(result.status).toBe('max_iterations');
|
||||
expect(result.iterations).toBe(2);
|
||||
@@ -344,31 +345,34 @@ describe('AgentLoopService', () => {
|
||||
|
||||
test('should handle tool execution error gracefully', async () => {
|
||||
const mockToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'invalid_tool',
|
||||
arguments: '{}'
|
||||
}
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: { name: 'invalid_tool', arguments: '{}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: 'tool_calls' as const,
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
const mockErrorResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I encountered an error while executing the tool.',
|
||||
tool_calls: undefined
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'I encountered an error while executing the tool.',
|
||||
tool_calls: undefined,
|
||||
},
|
||||
finish_reason: 'stop' as const,
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
const mockToolExecution: AgentToolExecution = {
|
||||
@@ -380,7 +384,7 @@ describe('AgentLoopService', () => {
|
||||
startTime: Date.now() - 100,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 100,
|
||||
errorMessage: 'Tool not found'
|
||||
errorMessage: 'Tool not found',
|
||||
};
|
||||
|
||||
const mockRegistryInstance = mockToolRegistry.getInstance();
|
||||
@@ -390,10 +394,7 @@ describe('AgentLoopService', () => {
|
||||
.mockResolvedValueOnce({ data: mockToolCallResponse })
|
||||
.mockResolvedValueOnce({ data: mockErrorResponse });
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Test error handling',
|
||||
'conv_123'
|
||||
);
|
||||
const result = await service.executeTask('Test error handling', 'conv_123');
|
||||
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.toolExecutions).toHaveLength(1);
|
||||
@@ -404,10 +405,7 @@ describe('AgentLoopService', () => {
|
||||
test('should handle API client errors', async () => {
|
||||
mockApiClient.post.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await service.executeTask(
|
||||
'Test API error',
|
||||
'conv_123'
|
||||
);
|
||||
const result = await service.executeTask('Test API error', 'conv_123');
|
||||
|
||||
expect(result.status).toBe('error');
|
||||
expect(result.error).toContain('Network error');
|
||||
@@ -417,31 +415,33 @@ describe('AgentLoopService', () => {
|
||||
|
||||
test('should parse tool name from function name correctly', async () => {
|
||||
const mockToolCallResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues', // Should parse to skillId=github, toolName=list_issues
|
||||
arguments: '{"owner":"user","repo":"test"}'
|
||||
}
|
||||
}]
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call_1',
|
||||
type: 'function' as const,
|
||||
function: {
|
||||
name: 'github_list_issues', // Should parse to skillId=github, toolName=list_issues
|
||||
arguments: '{"owner":"user","repo":"test"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: 'tool_calls' as const,
|
||||
},
|
||||
finish_reason: 'tool_calls' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
const mockFinalResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Done',
|
||||
tool_calls: undefined
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant' as const, content: 'Done', tool_calls: undefined },
|
||||
finish_reason: 'stop' as const,
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
const mockToolExecution: AgentToolExecution = {
|
||||
@@ -453,7 +453,7 @@ describe('AgentLoopService', () => {
|
||||
startTime: Date.now() - 100,
|
||||
endTime: Date.now(),
|
||||
executionTimeMs: 100,
|
||||
result: '{}'
|
||||
result: '{}',
|
||||
};
|
||||
|
||||
const mockRegistryInstance = mockToolRegistry.getInstance();
|
||||
@@ -486,13 +486,12 @@ describe('AgentLoopService', () => {
|
||||
describe('task execution options', () => {
|
||||
test('should use default options when none provided', async () => {
|
||||
const mockResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Test response'
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant' as const, content: 'Test response' },
|
||||
finish_reason: 'stop' as const,
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.post.mockResolvedValue({ data: mockResponse });
|
||||
@@ -506,13 +505,12 @@ describe('AgentLoopService', () => {
|
||||
|
||||
test('should respect custom execution options', async () => {
|
||||
const mockResponse = {
|
||||
choices: [{
|
||||
message: {
|
||||
role: 'assistant' as const,
|
||||
content: 'Test response'
|
||||
choices: [
|
||||
{
|
||||
message: { role: 'assistant' as const, content: 'Test response' },
|
||||
finish_reason: 'stop' as const,
|
||||
},
|
||||
finish_reason: 'stop' as const
|
||||
}]
|
||||
],
|
||||
};
|
||||
|
||||
mockApiClient.post.mockResolvedValue({ data: mockResponse });
|
||||
@@ -521,7 +519,7 @@ describe('AgentLoopService', () => {
|
||||
maxIterations: 3,
|
||||
timeoutMs: 5000,
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0.7
|
||||
temperature: 0.7,
|
||||
};
|
||||
|
||||
const result = await service.executeTask('Test', 'conv_123', customOptions);
|
||||
@@ -531,11 +529,8 @@ describe('AgentLoopService', () => {
|
||||
// Verify custom options were passed to API
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/conv_123/messages',
|
||||
expect.objectContaining({
|
||||
model: 'gpt-3.5-turbo',
|
||||
temperature: 0.7
|
||||
})
|
||||
expect.objectContaining({ model: 'gpt-3.5-turbo', temperature: 0.7 })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, test, expect, beforeEach, vi, type Mock } from 'vitest';
|
||||
import { AgentToolRegistry } from '../agentToolRegistry';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { beforeEach, describe, expect, type Mock, test, vi } from 'vitest';
|
||||
|
||||
import { AgentToolRegistry } from '../agentToolRegistry';
|
||||
|
||||
// Mock Tauri invoke
|
||||
vi.mock('@tauri-apps/api/core');
|
||||
@@ -19,35 +20,35 @@ describe('AgentToolRegistry', () => {
|
||||
test('should load tool schemas from Tauri using ZeroClaw format', async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "github_list_issues",
|
||||
description: "List GitHub issues for a repository",
|
||||
name: 'github_list_issues',
|
||||
description: 'List GitHub issues for a repository',
|
||||
parameters: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
owner: { type: "string", description: "Repository owner" },
|
||||
repo: { type: "string", description: "Repository name" }
|
||||
owner: { type: 'string', description: 'Repository owner' },
|
||||
repo: { type: 'string', description: 'Repository name' },
|
||||
},
|
||||
required: ["owner", "repo"]
|
||||
}
|
||||
}
|
||||
required: ['owner', 'repo'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "notion_create_page",
|
||||
description: "Create a new Notion page",
|
||||
name: 'notion_create_page',
|
||||
description: 'Create a new Notion page',
|
||||
parameters: {
|
||||
type: "object",
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: "string", description: "Page title" },
|
||||
content: { type: "string", description: "Page content" }
|
||||
title: { type: 'string', description: 'Page title' },
|
||||
content: { type: 'string', description: 'Page content' },
|
||||
},
|
||||
required: ["title"]
|
||||
}
|
||||
}
|
||||
}
|
||||
required: ['title'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
@@ -55,21 +56,21 @@ describe('AgentToolRegistry', () => {
|
||||
const schemas = await service.loadToolSchemas();
|
||||
|
||||
expect(schemas).toHaveLength(2);
|
||||
expect(schemas[0].function.name).toBe("github_list_issues");
|
||||
expect(schemas[1].function.name).toBe("notion_create_page");
|
||||
expect(schemas[0].function.name).toBe('github_list_issues');
|
||||
expect(schemas[1].function.name).toBe('notion_create_page');
|
||||
expect(mockInvoke).toHaveBeenCalledWith('runtime_get_tool_schemas');
|
||||
});
|
||||
|
||||
test('should cache tool schemas to avoid repeated calls', async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
}
|
||||
name: 'test_tool',
|
||||
description: 'Test tool',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
@@ -87,13 +88,13 @@ describe('AgentToolRegistry', () => {
|
||||
test('should force reload when requested', async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
}
|
||||
name: 'test_tool',
|
||||
description: 'Test tool',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
@@ -120,7 +121,9 @@ describe('AgentToolRegistry', () => {
|
||||
const errorMessage = 'Failed to load tool schemas';
|
||||
mockInvoke.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
await expect(service.loadToolSchemas()).rejects.toThrow(`Failed to load tool schemas: Error: ${errorMessage}`);
|
||||
await expect(service.loadToolSchemas()).rejects.toThrow(
|
||||
`Failed to load tool schemas: Error: ${errorMessage}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,7 +133,7 @@ describe('AgentToolRegistry', () => {
|
||||
success: true,
|
||||
output: '{"issues": [{"title": "Bug fix", "number": 1}]}',
|
||||
error: null,
|
||||
execution_time: 1500
|
||||
execution_time: 1500,
|
||||
};
|
||||
|
||||
mockInvoke.mockResolvedValue(mockResult);
|
||||
@@ -150,7 +153,7 @@ describe('AgentToolRegistry', () => {
|
||||
// Verify correct tool_id format and arguments
|
||||
expect(mockInvoke).toHaveBeenCalledWith('runtime_execute_tool', {
|
||||
toolId: 'github_list_issues',
|
||||
arguments: '{"owner":"user","repo":"test"}'
|
||||
arguments: '{"owner":"user","repo":"test"}',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -159,16 +162,12 @@ describe('AgentToolRegistry', () => {
|
||||
success: false,
|
||||
output: '',
|
||||
error: 'Tool not found: invalid_tool',
|
||||
execution_time: 100
|
||||
execution_time: 100,
|
||||
};
|
||||
|
||||
mockInvoke.mockResolvedValue(mockResult);
|
||||
|
||||
const result = await service.executeTool(
|
||||
'invalid',
|
||||
'tool',
|
||||
'{}'
|
||||
);
|
||||
const result = await service.executeTool('invalid', 'tool', '{}');
|
||||
|
||||
expect(result.status).toBe('error');
|
||||
expect(result.errorMessage).toBe('Tool not found: invalid_tool');
|
||||
@@ -180,7 +179,7 @@ describe('AgentToolRegistry', () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
output: 'Success',
|
||||
error: null
|
||||
error: null,
|
||||
// No execution_time provided
|
||||
};
|
||||
|
||||
@@ -208,12 +207,7 @@ describe('AgentToolRegistry', () => {
|
||||
});
|
||||
|
||||
test('should generate unique execution IDs', async () => {
|
||||
const mockResult = {
|
||||
success: true,
|
||||
output: 'test',
|
||||
error: null,
|
||||
execution_time: 100
|
||||
};
|
||||
const mockResult = { success: true, output: 'test', error: null, execution_time: 100 };
|
||||
|
||||
mockInvoke.mockResolvedValue(mockResult);
|
||||
|
||||
@@ -230,29 +224,29 @@ describe('AgentToolRegistry', () => {
|
||||
beforeEach(async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "github_list_issues",
|
||||
description: "List GitHub issues",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
name: 'github_list_issues',
|
||||
description: 'List GitHub issues',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "github_create_issue",
|
||||
description: "Create GitHub issue",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
name: 'github_create_issue',
|
||||
description: 'Create GitHub issue',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "notion_create_page",
|
||||
description: "Create Notion page",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
}
|
||||
name: 'notion_create_page',
|
||||
description: 'Create Notion page',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
@@ -280,7 +274,7 @@ describe('AgentToolRegistry', () => {
|
||||
expect(tools.map(t => t.function.name)).toEqual([
|
||||
'github_list_issues',
|
||||
'github_create_issue',
|
||||
'notion_create_page'
|
||||
'notion_create_page',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -294,7 +288,7 @@ describe('AgentToolRegistry', () => {
|
||||
|
||||
expect(toolsBySkill.github.map(t => t.function.name)).toEqual([
|
||||
'github_list_issues',
|
||||
'github_create_issue'
|
||||
'github_create_issue',
|
||||
]);
|
||||
expect(toolsBySkill.notion[0].function.name).toBe('notion_create_page');
|
||||
});
|
||||
@@ -341,13 +335,13 @@ describe('AgentToolRegistry', () => {
|
||||
test('should clear cached tool schemas', async () => {
|
||||
const mockSchemas = [
|
||||
{
|
||||
type: "function",
|
||||
type: 'function',
|
||||
function: {
|
||||
name: "test_tool",
|
||||
description: "Test tool",
|
||||
parameters: { type: "object", properties: {} }
|
||||
}
|
||||
}
|
||||
name: 'test_tool',
|
||||
description: 'Test tool',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockInvoke.mockResolvedValue(mockSchemas);
|
||||
@@ -364,4 +358,4 @@ describe('AgentToolRegistry', () => {
|
||||
expect(mockInvoke).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,22 +6,14 @@
|
||||
* - runtime_get_tool_schemas: Get all tools in OpenAI-compatible format
|
||||
* - runtime_execute_tool: Execute a tool with enhanced validation and timing
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type {
|
||||
AgentToolSchema,
|
||||
AgentToolExecution,
|
||||
IAgentToolRegistry
|
||||
} from '../types/agent';
|
||||
|
||||
import type { AgentToolExecution, AgentToolSchema, IAgentToolRegistry } from '../types/agent';
|
||||
|
||||
// ZeroClaw format types from Rust
|
||||
interface ZeroClawToolSchema {
|
||||
type: string;
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: any;
|
||||
};
|
||||
function: { name: string; description: string; parameters: any };
|
||||
}
|
||||
|
||||
interface ZeroClawToolResult {
|
||||
@@ -51,7 +43,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
const now = Date.now();
|
||||
|
||||
// Return cached tools if still fresh
|
||||
if (!forceReload && this.toolSchemas.length > 0 && (now - this.lastLoadTime) < this.CACHE_TTL) {
|
||||
if (!forceReload && this.toolSchemas.length > 0 && now - this.lastLoadTime < this.CACHE_TTL) {
|
||||
return this.toolSchemas;
|
||||
}
|
||||
|
||||
@@ -69,8 +61,8 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
function: {
|
||||
name: tool.function.name,
|
||||
description: tool.function.description,
|
||||
parameters: tool.function.parameters
|
||||
}
|
||||
parameters: tool.function.parameters,
|
||||
},
|
||||
}));
|
||||
|
||||
this.lastLoadTime = now;
|
||||
@@ -110,7 +102,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
} catch (e) {
|
||||
return 'Failed to parse: ' + e;
|
||||
}
|
||||
})()
|
||||
})(),
|
||||
});
|
||||
|
||||
const execution: AgentToolExecution = {
|
||||
@@ -119,7 +111,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
skillId,
|
||||
arguments: toolArguments,
|
||||
status: 'running',
|
||||
startTime
|
||||
startTime,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -130,15 +122,15 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
console.log(` args type: ${typeof toolArguments}`);
|
||||
|
||||
const result = await invoke<ZeroClawToolResult>('runtime_execute_tool', {
|
||||
toolId: toolId, // Use camelCase as expected by current Rust version
|
||||
args: toolArguments // Use "args" instead of "arguments"
|
||||
toolId: toolId, // Use camelCase as expected by current Rust version
|
||||
args: toolArguments, // Use "args" instead of "arguments"
|
||||
});
|
||||
|
||||
console.log(`🔧 [AFTER INVOKE] Tool execution result:`, result);
|
||||
|
||||
execution.endTime = Date.now();
|
||||
// Use execution time from Rust if available, otherwise calculate locally
|
||||
execution.executionTimeMs = result.execution_time || (execution.endTime - execution.startTime);
|
||||
execution.executionTimeMs = result.execution_time || execution.endTime - execution.startTime;
|
||||
|
||||
if (!result.success) {
|
||||
execution.status = 'error';
|
||||
@@ -155,7 +147,6 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
}
|
||||
|
||||
return execution;
|
||||
|
||||
} catch (error) {
|
||||
execution.endTime = Date.now();
|
||||
execution.executionTimeMs = execution.endTime - execution.startTime;
|
||||
@@ -205,11 +196,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
/**
|
||||
* Get tool execution statistics
|
||||
*/
|
||||
getToolStats(): {
|
||||
totalTools: number;
|
||||
skillCount: number;
|
||||
categories: Record<string, number>;
|
||||
} {
|
||||
getToolStats(): { totalTools: number; skillCount: number; categories: Record<string, number> } {
|
||||
const categories: Record<string, number> = {};
|
||||
const skills = new Set<string>();
|
||||
|
||||
@@ -222,11 +209,7 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
categories[category] = (categories[category] || 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
totalTools: this.toolSchemas.length,
|
||||
skillCount: skills.size,
|
||||
categories
|
||||
};
|
||||
return { totalTools: this.toolSchemas.length, skillCount: skills.size, categories };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -272,4 +255,4 @@ export class AgentToolRegistry implements IAgentToolRegistry {
|
||||
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ export const actionableItemsApi = {
|
||||
* GET /telegram/actionable-items
|
||||
*/
|
||||
getActionableItems: async (): Promise<ActionableItem[]> => {
|
||||
const response = await apiClient.get<ApiResponse<ActionableItem[]>>('/telegram/actionable-items');
|
||||
const response = await apiClient.get<ApiResponse<ActionableItem[]>>(
|
||||
'/telegram/actionable-items'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
@@ -110,4 +112,4 @@ export const actionableItemsApi = {
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -50,4 +50,4 @@ export const apiKeysApi = {
|
||||
revokeApiKey: async (keyId: string): Promise<void> => {
|
||||
await apiClient.delete<ApiResponse<unknown>>(`/api-keys/${keyId}`);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -39,13 +39,10 @@ export const creditsApi = {
|
||||
* Get paginated credit transaction history
|
||||
* GET /credits/transactions
|
||||
*/
|
||||
getTransactions: async (
|
||||
page = 1,
|
||||
limit = 50
|
||||
): Promise<PaginatedTransactions> => {
|
||||
getTransactions: async (page = 1, limit = 50): Promise<PaginatedTransactions> => {
|
||||
const response = await apiClient.get<ApiResponse<PaginatedTransactions>>(
|
||||
`/credits/transactions?page=${page}&limit=${limit}`
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -65,4 +65,4 @@ export const feedbackApi = {
|
||||
const response = await apiClient.put<ApiResponse<FeedbackItem>>(`/feedback/${id}`, updates);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -35,4 +35,4 @@ export const settingsApi = {
|
||||
setPlatformsConnected: async (platforms: string[]): Promise<void> => {
|
||||
await apiClient.post<ApiResponse<unknown>>('/settings/platforms-connected', { platforms });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { injectAll } from '../../lib/ai/injector';
|
||||
import type { Message } from '../../lib/ai/providers/interface';
|
||||
import type { ApiResponse } from '../../types/api';
|
||||
import type {
|
||||
PurgeRequestBody,
|
||||
@@ -10,8 +12,6 @@ import type {
|
||||
ThreadsListData,
|
||||
} from '../../types/thread';
|
||||
import { apiClient } from '../apiClient';
|
||||
import { injectAll } from '../../lib/ai/injector';
|
||||
import type { Message } from '../../lib/ai/providers/interface';
|
||||
|
||||
export const threadApi = {
|
||||
/** GET /threads — list all threads for the authenticated user */
|
||||
@@ -55,14 +55,11 @@ export const threadApi = {
|
||||
|
||||
if (options.injectSoul) {
|
||||
try {
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: message }]
|
||||
};
|
||||
const userMessage: Message = { role: 'user', content: [{ type: 'text', text: message }] };
|
||||
|
||||
const injectedMessage = await injectAll(userMessage, {
|
||||
mode: 'context-block',
|
||||
includeMetadata: false
|
||||
includeMetadata: false,
|
||||
});
|
||||
|
||||
// Extract the processed text
|
||||
|
||||
@@ -92,10 +92,7 @@ const daemonSlice = createSlice({
|
||||
}
|
||||
},
|
||||
|
||||
setDaemonStatus: (
|
||||
state,
|
||||
action: PayloadAction<{ userId: string; status: DaemonStatus }>
|
||||
) => {
|
||||
setDaemonStatus: (state, action: PayloadAction<{ userId: string; status: DaemonStatus }>) => {
|
||||
const { userId, status } = action.payload;
|
||||
const user = ensureUserState(state, userId);
|
||||
user.status = status;
|
||||
@@ -107,37 +104,25 @@ const daemonSlice = createSlice({
|
||||
}
|
||||
},
|
||||
|
||||
incrementConnectionAttempts: (
|
||||
state,
|
||||
action: PayloadAction<{ userId: string }>
|
||||
) => {
|
||||
incrementConnectionAttempts: (state, action: PayloadAction<{ userId: string }>) => {
|
||||
const { userId } = action.payload;
|
||||
const user = ensureUserState(state, userId);
|
||||
user.connectionAttempts += 1;
|
||||
},
|
||||
|
||||
resetConnectionAttempts: (
|
||||
state,
|
||||
action: PayloadAction<{ userId: string }>
|
||||
) => {
|
||||
resetConnectionAttempts: (state, action: PayloadAction<{ userId: string }>) => {
|
||||
const { userId } = action.payload;
|
||||
const user = ensureUserState(state, userId);
|
||||
user.connectionAttempts = 0;
|
||||
},
|
||||
|
||||
setAutoStartEnabled: (
|
||||
state,
|
||||
action: PayloadAction<{ userId: string; enabled: boolean }>
|
||||
) => {
|
||||
setAutoStartEnabled: (state, action: PayloadAction<{ userId: string; enabled: boolean }>) => {
|
||||
const { userId, enabled } = action.payload;
|
||||
const user = ensureUserState(state, userId);
|
||||
user.autoStartEnabled = enabled;
|
||||
},
|
||||
|
||||
setIsRecovering: (
|
||||
state,
|
||||
action: PayloadAction<{ userId: string; isRecovering: boolean }>
|
||||
) => {
|
||||
setIsRecovering: (state, action: PayloadAction<{ userId: string; isRecovering: boolean }>) => {
|
||||
const { userId, isRecovering } = action.payload;
|
||||
const user = ensureUserState(state, userId);
|
||||
user.isRecovering = isRecovering;
|
||||
@@ -211,4 +196,4 @@ export const selectIsDaemonRecovering = (state: { daemon: DaemonState }, userId?
|
||||
return daemonState.isRecovering;
|
||||
};
|
||||
|
||||
export default daemonSlice.reducer;
|
||||
export default daemonSlice.reducer;
|
||||
|
||||
@@ -53,7 +53,6 @@ const threadPersistConfig = {
|
||||
whitelist: ['panelWidth', 'lastViewedAt', 'threads', 'messagesByThreadId', 'selectedThreadId'],
|
||||
};
|
||||
|
||||
|
||||
const persistedAuthReducer = persistReducer(authPersistConfig, authReducer);
|
||||
const persistedAiReducer = persistReducer(aiPersistConfig, aiReducer);
|
||||
const persistedSkillsReducer = persistReducer(skillsPersistConfig, skillsReducer);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { threadApi } from '../services/api/threadApi';
|
||||
import type { Thread, ThreadMessage } from '../types/thread';
|
||||
import { injectAll } from '../lib/ai/injector';
|
||||
import type { Message } from '../lib/ai/providers/interface';
|
||||
import { threadApi } from '../services/api/threadApi';
|
||||
import type { Thread, ThreadMessage } from '../types/thread';
|
||||
import type { RootState } from './index';
|
||||
|
||||
interface ThreadState {
|
||||
@@ -101,14 +101,11 @@ export const sendMessage = createAsyncThunk(
|
||||
// 2. Process message with SOUL + TOOLS injection before sending to API
|
||||
let processedMessage = message;
|
||||
try {
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: message }]
|
||||
};
|
||||
const userMessage: Message = { role: 'user', content: [{ type: 'text', text: message }] };
|
||||
|
||||
const injectedMessage = await injectAll(userMessage, {
|
||||
mode: 'context-block',
|
||||
includeMetadata: false
|
||||
includeMetadata: false,
|
||||
});
|
||||
|
||||
// Extract the processed text
|
||||
@@ -119,7 +116,10 @@ export const sendMessage = createAsyncThunk(
|
||||
|
||||
console.log('✅ SOUL + TOOLS injection successful in Redux sendMessage thunk');
|
||||
} catch (injectionError) {
|
||||
console.warn('⚠️ SOUL + TOOLS injection failed in Redux sendMessage thunk:', injectionError);
|
||||
console.warn(
|
||||
'⚠️ SOUL + TOOLS injection failed in Redux sendMessage thunk:',
|
||||
injectionError
|
||||
);
|
||||
// Continue with original message
|
||||
}
|
||||
|
||||
|
||||
Vendored
+2
-4
@@ -2,10 +2,8 @@
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__TAURI__?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
__TAURI__?: { [key: string]: unknown };
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
export {};
|
||||
|
||||
+1
-1
@@ -23,4 +23,4 @@ export interface OAuthError {
|
||||
provider: OAuthProvider;
|
||||
message: string;
|
||||
code?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
|
||||
import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
|
||||
import { skillManager } from '../lib/skills/manager';
|
||||
import { consumeLoginToken, fetchIntegrationTokens } from '../services/api/authApi';
|
||||
import { buildManualSentryEvent, enqueueError } from '../services/errorReportQueue';
|
||||
import { store } from '../store';
|
||||
import { setToken } from '../store/authSlice';
|
||||
import { setSkillState } from '../store/skillsSlice';
|
||||
import { skillManager } from '../lib/skills/manager';
|
||||
import {
|
||||
decryptIntegrationTokens,
|
||||
hexToBase64,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Helper functions for invoking Tauri commands from the frontend.
|
||||
*/
|
||||
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { injectAll } from '../lib/ai/injector';
|
||||
import type { Message } from '../lib/ai/providers/interface';
|
||||
|
||||
@@ -405,14 +406,11 @@ export async function alphahumanAgentChat(
|
||||
|
||||
if (options.injectSoul) {
|
||||
try {
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: message }]
|
||||
};
|
||||
const userMessage: Message = { role: 'user', content: [{ type: 'text', text: message }] };
|
||||
|
||||
const injectedMessage = await injectAll(userMessage, {
|
||||
mode: 'context-block',
|
||||
includeMetadata: false
|
||||
includeMetadata: false,
|
||||
});
|
||||
|
||||
// Extract the processed text
|
||||
|
||||
Reference in New Issue
Block a user