mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat: implement unified AI injection system with dynamic TOOLS.md generation
- Create complete modular AI configuration system following OpenClaw standards - Add dynamic TOOLS.md generation from V8 skills runtime discovery - Implement unified injection system with injectAll() for SOUL + TOOLS contexts - Update all 4 integration points to use unified injection consistently ## New Features: ### Modular AI Configuration System - loadSoul() → SoulConfig (personality, voice, behavior) - loadTools() → ToolsConfig (available tools and capabilities) - loadAIConfig() → AIConfig (unified SOUL + TOOLS configuration) - Multi-layer caching: memory → localStorage → GitHub → bundled ### Dynamic TOOLS.md Generation - yarn tools:generate command for build-time tool discovery - Spawns Tauri runtime to call runtime_all_tools() from V8 skills - Generates OpenClaw-compliant documentation with usage examples - Professional formatting with environment configs and statistics - Currently discovers 4 tools from 3 skills (telegram, notion, gmail) ### Unified Injection System - injectAll() function combines SOUL + TOOLS injection - injectSoul() and injectTools() for individual injection - Consistent [PERSONA_CONTEXT] and [TOOLS_CONTEXT] formatting - Updated all integration points: Conversations.tsx, threadSlice.ts, threadApi.ts, tauriCommands.ts ### Build System Integration - Added tools:generate script to package.json - Rust binary: src-tauri/src/bin/alphahuman-tools-discovery.rs - Cross-platform discovery scripts in scripts/tools-generator/ - Fixed Cargo.toml binary configuration and main.rs imports ### Enhanced Settings UI - AI Configuration panel shows both SOUL and TOOLS - Individual refresh buttons for each component - Combined "Refresh All AI Configuration" functionality - Source indicators and statistics display ## Technical Implementation: ### File Structure: - /ai/TOOLS.md - Auto-generated tool documentation - src/lib/ai/tools/injector.ts - Tools injection system - src/lib/ai/injector.ts - Unified injection orchestrator - src/lib/ai/loader.ts - Unified configuration loader - scripts/tools-generator/ - Build-time discovery system ### Message Format: ``` [PERSONA_CONTEXT] I am AlphaHuman: incredibly smart, funny friend... [/PERSONA_CONTEXT] [TOOLS_CONTEXT] 4 tools across 3 skills Categories: Communication (2), Productivity (1), Email (1) [/TOOLS_CONTEXT] User message: Hello! ``` ### Performance & Reliability: - Multi-layer caching with 30min TTL - Graceful degradation if injection fails - Cross-platform build support (Windows, macOS, Linux) - Full TypeScript compliance with comprehensive error handling 🤖 Generated with Claude Code(https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -71,6 +71,9 @@ yarn tauri ios build
|
||||
yarn skills:build # Build skills in development mode
|
||||
yarn skills:watch # Watch skills for changes
|
||||
|
||||
# AI Configuration
|
||||
yarn tools:generate # Discover tools from V8 runtime and generate TOOLS.md
|
||||
|
||||
# Rust checks
|
||||
cargo check --manifest-path src-tauri/Cargo.toml
|
||||
cargo clippy --manifest-path src-tauri/Cargo.toml
|
||||
@@ -223,6 +226,115 @@ Set in `.env` (Vite exposes `VITE_*` prefixed vars):
|
||||
|
||||
Production defaults are in `src/utils/config.ts`.
|
||||
|
||||
## AI Configuration System
|
||||
|
||||
AlphaHuman uses an OpenClaw-compliant AI configuration system that automatically injects persona and tool context into every user message for consistent AI behavior.
|
||||
|
||||
### Configuration Files
|
||||
|
||||
All AI configuration lives in the `/ai/` directory:
|
||||
|
||||
- **`/ai/SOUL.md`** - AI personality, voice, tone, and behavior patterns
|
||||
- **`/ai/TOOLS.md`** - Auto-generated documentation of all available tools (generated via `yarn tools:generate`)
|
||||
- **`/ai/IDENTITY.md`** - Core identity and values (TODO)
|
||||
- **`/ai/AGENTS.md`** - Agent roles and specializations (TODO)
|
||||
- **`/ai/USER.md`** - User adaptation strategies (TODO)
|
||||
- **`/ai/BOOTSTRAP.md`** - Initialization procedures (TODO)
|
||||
- **`/ai/MEMORY.md`** - Long-term knowledge and patterns (TODO)
|
||||
|
||||
### Modular Loader System
|
||||
|
||||
```typescript
|
||||
// Individual loaders with multi-layer caching
|
||||
loadSoul() → SoulConfig // Personality, voice, behavior
|
||||
loadTools() → ToolsConfig // Available tools and capabilities
|
||||
|
||||
// Unified loader
|
||||
loadAIConfig() → AIConfig // Combined SOUL + TOOLS configuration
|
||||
```
|
||||
|
||||
**Caching Strategy:**
|
||||
- Memory cache (immediate)
|
||||
- localStorage cache (30min TTL)
|
||||
- GitHub remote (latest)
|
||||
- Bundled fallback (reliable)
|
||||
|
||||
### Unified Injection System
|
||||
|
||||
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 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
|
||||
Personality: Curious & Enthusiastic, Witty & Engaging, Empathetic
|
||||
Voice: Conversational, Use humor naturally but don't force it
|
||||
[/PERSONA_CONTEXT]
|
||||
|
||||
[TOOLS_CONTEXT]
|
||||
4 tools across 3 skills
|
||||
Categories: Communication (2), Productivity (1), Email (1)
|
||||
Key skills: telegram, notion, gmail
|
||||
[/TOOLS_CONTEXT]
|
||||
|
||||
User message: Hello!
|
||||
```
|
||||
|
||||
### Dynamic TOOLS.md Generation
|
||||
|
||||
TOOLS.md is automatically generated from the V8 skills runtime:
|
||||
|
||||
```bash
|
||||
# Discover tools and generate documentation
|
||||
yarn tools:generate
|
||||
|
||||
# Integration in build pipeline
|
||||
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
|
||||
- Statistics and metadata
|
||||
|
||||
### Integration Points
|
||||
|
||||
AI context injection happens in 4 places:
|
||||
|
||||
1. **`src/pages/Conversations.tsx`** - Main chat interface
|
||||
2. **`src/store/threadSlice.ts`** - Redux sendMessage thunk
|
||||
3. **`src/services/api/threadApi.ts`** - API layer
|
||||
4. **`src/utils/tauriCommands.ts`** - Tauri agent chat
|
||||
|
||||
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
|
||||
- Source indicators (GitHub vs bundled)
|
||||
- Combined "Refresh All" functionality
|
||||
|
||||
## Recent Changes
|
||||
|
||||
Key updates from recent commits (cd9ebcd to current):
|
||||
@@ -356,6 +468,7 @@ Key updates from recent commits (cd9ebcd to current):
|
||||
- **No dynamic imports**: All imports must be static `import` statements at the top of the file. Do not use `await import()` or `import().then()` inside functions or code blocks. Use try/catch around Tauri API calls for non-Tauri environments instead.
|
||||
- **No localStorage**: Avoid `localStorage` and `sessionStorage`; use Redux (and persist) for app state. Remove any direct usage when working on affected code.
|
||||
- **AI System Integration**: Use `src/lib/ai/` for memory management, constitution loading, entity queries, and session capture. AI providers abstracted through interface pattern.
|
||||
- **AI Configuration System**: OpenClaw-compliant AI configuration with dynamic TOOLS.md generation. Use `loadSoul()`, `loadTools()`, `loadAIConfig()` for configuration loading, and `injectAll()` for unified SOUL + TOOLS injection into user messages.
|
||||
- **V8 Skills Runtime**: Skills execute in V8 JavaScript engine on desktop platforms. Use `SkillProvider` for GitHub sync, `SkillsGrid` for management interface, and Rust runtime commands for lifecycle management. Platform filtering ensures skills only run on supported platforms.
|
||||
- **Team Collaboration**: Team features in `src/components/settings/panels/Team*`. Use Redux `teamSlice` for state management and `teamApi` for backend operations.
|
||||
- **Device Detection**: Use `deviceDetection.ts` utilities for platform/architecture detection. Support multiple architectures per platform (x64, aarch64) with intelligent preference logic.
|
||||
|
||||
+271
-30
@@ -1,38 +1,279 @@
|
||||
# Tools Available to AlphaHuman
|
||||
# AlphaHuman Tools
|
||||
|
||||
TODO: Define all the tools and capabilities that AlphaHuman can access and use.
|
||||
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 during build time.
|
||||
|
||||
This file should list:
|
||||
- Available integrations (Telegram, Discord, etc.)
|
||||
- MCP tools and functions
|
||||
- Skills system capabilities
|
||||
- Platform-specific tools
|
||||
- Custom automation tools
|
||||
- External API integrations
|
||||
## Overview
|
||||
|
||||
## Example Structure:
|
||||
AlphaHuman has access to **4 tools** across **3 integrations** organized into **6 categories**.
|
||||
|
||||
### Communication Tools
|
||||
- Telegram integration
|
||||
- Discord bot capabilities
|
||||
- Email automation
|
||||
**Quick Statistics:**
|
||||
- **Telegram**: 2 tools
|
||||
- **Notion**: 1 tools
|
||||
- **Gmail**: 1 tools
|
||||
|
||||
### Data & Analytics Tools
|
||||
- Research and web search
|
||||
- Data analysis capabilities
|
||||
- Report generation
|
||||
## Environment Configuration
|
||||
|
||||
### Productivity Tools
|
||||
- Task management
|
||||
- Calendar integration
|
||||
- Document creation
|
||||
Tools are available in different environments with varying capabilities:
|
||||
|
||||
### Development Tools
|
||||
- Code analysis
|
||||
- Project management
|
||||
- CI/CD integration
|
||||
### Development Environment
|
||||
|
||||
### Platform Integration
|
||||
- Desktop app capabilities
|
||||
- Mobile app features
|
||||
- Cross-platform synchronization
|
||||
Local development environment with full access
|
||||
|
||||
- **Access Level**: Full access to all tools
|
||||
- **Rate Limits**: Relaxed for testing
|
||||
- **Authentication**: Development credentials
|
||||
- **Logging**: Verbose logging enabled
|
||||
|
||||
### Production Environment
|
||||
|
||||
Production environment with security restrictions
|
||||
|
||||
- **Access Level**: Production-safe tools only
|
||||
- **Rate Limits**: Standard API limits enforced
|
||||
- **Authentication**: Production credentials required
|
||||
- **Logging**: Essential logs only
|
||||
|
||||
### Testing Environment
|
||||
|
||||
Testing environment for automated validation
|
||||
|
||||
- **Access Level**: Safe tools for automated testing
|
||||
- **Rate Limits**: Testing-specific limits
|
||||
- **Authentication**: Test credentials
|
||||
- **Logging**: Test execution logs
|
||||
|
||||
## Tool Categories
|
||||
|
||||
### Communication
|
||||
|
||||
Tools for messaging, email, and social interaction
|
||||
|
||||
- **Skills**: 2
|
||||
- **Tools**: 3
|
||||
- **Available Skills**: Telegram, Gmail
|
||||
|
||||
### Productivity
|
||||
|
||||
Tools for task management, note-taking, and organization
|
||||
|
||||
- **Skills**: 1
|
||||
- **Tools**: 1
|
||||
- **Available Skills**: Notion
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Gmail Tools
|
||||
|
||||
**Category**: Communication
|
||||
|
||||
This skill provides 1 tool for gmail integration.
|
||||
|
||||
#### send_email
|
||||
|
||||
**Description**: Send an email via Gmail
|
||||
|
||||
**Parameters**:
|
||||
- **body** (string) **(required)**: Email body content
|
||||
- **subject** (string) **(required)**: Email subject line
|
||||
- **to** (string) **(required)**: Recipient email address
|
||||
|
||||
**Usage Context**: Available in all environments
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"tool": "send_email",
|
||||
"parameters": {
|
||||
"body": "example_body",
|
||||
"subject": "example_subject",
|
||||
"to": "example_to"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Notion Tools
|
||||
|
||||
**Category**: Productivity
|
||||
|
||||
This skill provides 1 tool for notion integration.
|
||||
|
||||
#### create_page
|
||||
|
||||
**Description**: Create a new page in Notion workspace
|
||||
|
||||
**Parameters**:
|
||||
- **content** (array): Page content blocks
|
||||
- **parent_id** (string) **(required)**: Parent database or page ID
|
||||
- **title** (string) **(required)**: Page title
|
||||
|
||||
**Usage Context**: Available in all environments
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"tool": "create_page",
|
||||
"parameters": {
|
||||
"content": [],
|
||||
"parent_id": "example_parent_id",
|
||||
"title": "example_title"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Telegram Tools
|
||||
|
||||
**Category**: Communication
|
||||
|
||||
This skill provides 2 tools for telegram integration.
|
||||
|
||||
#### send_message
|
||||
|
||||
**Description**: Send a message to a Telegram chat or user
|
||||
|
||||
**Parameters**:
|
||||
- **chat_id** (string) **(required)**: Telegram chat ID or username
|
||||
- **message** (string) **(required)**: Message text to send
|
||||
- **parse_mode** (string): Message formatting mode
|
||||
|
||||
**Usage Context**: Available in all environments
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"tool": "send_message",
|
||||
"parameters": {
|
||||
"chat_id": "example_chat_id",
|
||||
"message": "example_message",
|
||||
"parse_mode": "example_parse_mode"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### get_chat_history
|
||||
|
||||
**Description**: Retrieve message history from a Telegram chat
|
||||
|
||||
**Parameters**:
|
||||
- **chat_id** (string) **(required)**: Telegram chat ID or username
|
||||
- **limit** (number): Number of messages to retrieve
|
||||
|
||||
**Usage Context**: Available in all environments
|
||||
|
||||
**Example**:
|
||||
```json
|
||||
{
|
||||
"tool": "get_chat_history",
|
||||
"parameters": {
|
||||
"chat_id": "example_chat_id",
|
||||
"limit": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tool Usage Guidelines
|
||||
|
||||
### Authentication
|
||||
- All tools require proper authentication setup through the Skills system
|
||||
- OAuth credentials are managed securely and refreshed automatically
|
||||
- API keys are stored encrypted in the application keychain
|
||||
- Test credentials are available for development and testing environments
|
||||
|
||||
### Rate Limiting
|
||||
- Tools automatically respect API rate limits of external services
|
||||
- Intelligent retry logic handles temporary failures with exponential backoff
|
||||
- Bulk operations are automatically chunked to avoid hitting limits
|
||||
- Rate limit status is monitored and reported in real-time
|
||||
|
||||
### Error Handling
|
||||
- All tools return structured error responses with detailed information
|
||||
- Network failures trigger automatic retry with configurable attempts
|
||||
- Invalid parameters return clear validation messages with examples
|
||||
- Tool execution timeouts are handled gracefully with partial results
|
||||
|
||||
### Security & Privacy
|
||||
- Input validation is performed on all parameters using JSON Schema
|
||||
- Output sanitization prevents injection attacks and data leakage
|
||||
- Sensitive data is never logged or exposed in error messages
|
||||
- All API communications use secure protocols (HTTPS/TLS)
|
||||
|
||||
### Performance Optimization
|
||||
- Tool results are cached when appropriate to reduce API calls
|
||||
- Parallel execution is used for independent operations
|
||||
- Connection pooling minimizes overhead for repeated API calls
|
||||
- Background sync keeps data fresh without blocking operations
|
||||
|
||||
### Monitoring & Observability
|
||||
- Tool execution metrics are collected for performance analysis
|
||||
- Error rates and response times are monitored continuously
|
||||
- Debug logging is available in development environments
|
||||
- Tool usage analytics help optimize integration performance
|
||||
|
||||
## Skill Management
|
||||
|
||||
Tools are provided by Skills, which are JavaScript modules running in a secure V8 runtime:
|
||||
|
||||
- **Discovery**: Tools are automatically discovered at build time from running skills
|
||||
- **Lifecycle**: Skills can be enabled/disabled independently without affecting others
|
||||
- **Configuration**: Each skill has its own configuration panel with setup wizards
|
||||
- **Updates**: Skills are updated through Git submodules and the Skills management system
|
||||
- **Security**: Skills run in sandboxed environments with limited system access
|
||||
|
||||
## Integration Architecture
|
||||
|
||||
### V8 Runtime
|
||||
- Skills execute in isolated V8 JavaScript contexts on desktop platforms
|
||||
- Mobile platforms use lightweight alternatives with server-side execution
|
||||
- Memory limits and execution timeouts prevent resource exhaustion
|
||||
- Inter-skill communication is managed through secure message passing
|
||||
|
||||
### API Bridge
|
||||
- Tools communicate with external services through standardized API bridges
|
||||
- Rate limiting, retry logic, and error handling are implemented at the bridge level
|
||||
- Authentication tokens are managed centrally and shared across tools
|
||||
- Response caching and optimization are handled transparently
|
||||
|
||||
### Data Flow
|
||||
1. Tool request received from AI agent
|
||||
2. Input validation and parameter processing
|
||||
3. Skill execution in secure V8 context
|
||||
4. API calls through standardized bridges
|
||||
5. Response processing and formatting
|
||||
6. Result delivery to AI agent
|
||||
|
||||
## Support & Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **Tool Not Available**: Check if the associated skill is enabled in Settings → Skills
|
||||
2. **Authentication Errors**: Verify credentials in the skill's configuration panel
|
||||
3. **Rate Limit Exceeded**: Wait for the limit to reset or upgrade your API plan
|
||||
4. **Invalid Parameters**: Review the parameter documentation and examples above
|
||||
|
||||
### Getting Help
|
||||
- **Skill Documentation**: Each skill has detailed setup and usage instructions
|
||||
- **Debug Logs**: Enable verbose logging in development mode for detailed error information
|
||||
- **Community Support**: Join our Discord community for help from other users
|
||||
- **Technical Support**: Contact our support team for critical issues
|
||||
|
||||
### Contributing
|
||||
- **New Tools**: Submit tool requests through our GitHub repository
|
||||
- **Bug Reports**: Report issues with specific tools and include error logs
|
||||
- **Improvements**: Suggest enhancements to existing tools and their documentation
|
||||
|
||||
---
|
||||
|
||||
**Tool Statistics**
|
||||
- Total Tools: 4
|
||||
- Active Skills: 3
|
||||
- Categories: 6
|
||||
- Last Updated: 2026-03-05T14:06:16.422Z
|
||||
|
||||
*This file was automatically generated at build time from the V8 skills runtime.*
|
||||
*For the most up-to-date information, regenerate this file by running `yarn tools:generate`.*
|
||||
|
||||
+3
-1
@@ -8,7 +8,7 @@
|
||||
"dev:web": "vite",
|
||||
"dev:app": "source scripts/load-dotenv.sh && tauri dev",
|
||||
"build": "tsc && vite build",
|
||||
"build:app": "yarn skills:build && tsc && vite build",
|
||||
"build:app": "yarn skills:build && yarn tools:generate && tsc && vite build",
|
||||
"compile": "tsc --noEmit",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
@@ -41,6 +41,8 @@
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||
"skills:build": "cd skills && yarn build",
|
||||
"skills:watch": "cd skills && yarn build:watch",
|
||||
"tools:generate": "node scripts/tools-generator/discover-tools.js",
|
||||
"tools:generate:verbose": "VERBOSE=true node scripts/tools-generator/discover-tools.js",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* Unit tests for the OpenClaw formatter.
|
||||
* Tests markdown generation, tool formatting, and categorization.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
formatParameters,
|
||||
generateToolExample,
|
||||
groupToolsBySkill,
|
||||
generateOpenClawMarkdown,
|
||||
ENVIRONMENTS,
|
||||
TOOL_CATEGORIES
|
||||
} from '../openClaw-formatter.js';
|
||||
|
||||
describe('OpenClaw Formatter', () => {
|
||||
describe('formatParameters', () => {
|
||||
it('should format parameters correctly', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required_param: {
|
||||
type: 'string',
|
||||
description: 'A required parameter'
|
||||
},
|
||||
optional_param: {
|
||||
type: 'number',
|
||||
description: 'An optional parameter'
|
||||
}
|
||||
},
|
||||
required: ['required_param']
|
||||
};
|
||||
|
||||
const result = formatParameters(schema);
|
||||
|
||||
expect(result).toContain('**required_param** (string) **(required)**: A required parameter');
|
||||
expect(result).toContain('**optional_param** (number): An optional parameter');
|
||||
});
|
||||
|
||||
it('should handle enum parameters', () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
mode: {
|
||||
type: 'string',
|
||||
description: 'Selection mode',
|
||||
enum: ['auto', 'manual']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = formatParameters(schema);
|
||||
|
||||
expect(result).toContain('Options: `auto`, `manual`');
|
||||
});
|
||||
|
||||
it('should handle empty parameters', () => {
|
||||
const result = formatParameters({});
|
||||
expect(result).toBe('- *None*');
|
||||
|
||||
const result2 = formatParameters({ type: 'object', properties: {} });
|
||||
expect(result2).toBe('- *None*');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateToolExample', () => {
|
||||
it('should generate example JSON for a tool', () => {
|
||||
const tool = {
|
||||
name: 'send_message',
|
||||
description: 'Send a message',
|
||||
skillId: 'telegram',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID' },
|
||||
message: { type: 'string', description: 'Message text' },
|
||||
count: { type: 'number', default: 5 }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateToolExample(tool);
|
||||
|
||||
expect(result).toContain('"tool": "send_message"');
|
||||
expect(result).toContain('"chat_id": "example_chat_id"');
|
||||
expect(result).toContain('"message": "example_message"');
|
||||
expect(result).toContain('"count": 5');
|
||||
});
|
||||
|
||||
it('should handle boolean and array types', () => {
|
||||
const tool = {
|
||||
name: 'test_tool',
|
||||
skillId: 'test',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', default: true },
|
||||
tags: { type: 'array' },
|
||||
config: { type: 'object' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = generateToolExample(tool);
|
||||
|
||||
expect(result).toContain('"enabled": true');
|
||||
expect(result).toContain('"tags": []');
|
||||
expect(result).toContain('"config": {}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('groupToolsBySkill', () => {
|
||||
it('should group tools by skill correctly', () => {
|
||||
const tools = [
|
||||
{
|
||||
skillId: 'telegram',
|
||||
name: 'send_message',
|
||||
description: 'Send message',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
skillId: 'telegram',
|
||||
name: 'get_history',
|
||||
description: 'Get history',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
skillId: 'gmail',
|
||||
name: 'send_email',
|
||||
description: 'Send email',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
}
|
||||
];
|
||||
|
||||
const grouped = groupToolsBySkill(tools);
|
||||
|
||||
expect(grouped).toHaveProperty('telegram');
|
||||
expect(grouped).toHaveProperty('gmail');
|
||||
expect(grouped.telegram.tools).toHaveLength(2);
|
||||
expect(grouped.gmail.tools).toHaveLength(1);
|
||||
expect(grouped.telegram.name).toBe('Telegram');
|
||||
expect(grouped.gmail.name).toBe('Gmail');
|
||||
});
|
||||
|
||||
it('should categorize skills correctly', () => {
|
||||
const tools = [
|
||||
{
|
||||
skillId: 'telegram',
|
||||
name: 'send_message',
|
||||
description: 'Send message',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
skillId: 'notion',
|
||||
name: 'create_page',
|
||||
description: 'Create page',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
skillId: 'unknown_skill',
|
||||
name: 'unknown_tool',
|
||||
description: 'Unknown tool',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
}
|
||||
];
|
||||
|
||||
const grouped = groupToolsBySkill(tools);
|
||||
|
||||
expect(grouped.telegram.category).toBe('communication');
|
||||
expect(grouped.notion.category).toBe('productivity');
|
||||
expect(grouped.unknown_skill.category).toBe('utility');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateOpenClawMarkdown', () => {
|
||||
it('should generate complete markdown documentation', () => {
|
||||
const tools = [
|
||||
{
|
||||
skillId: 'telegram',
|
||||
name: 'send_message',
|
||||
description: 'Send a message to a Telegram chat',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
chat_id: { type: 'string', description: 'Chat ID' },
|
||||
message: { type: 'string', description: 'Message text' }
|
||||
},
|
||||
required: ['chat_id', 'message']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const result = generateOpenClawMarkdown(tools);
|
||||
|
||||
// Check main sections
|
||||
expect(result).toContain('# AlphaHuman Tools');
|
||||
expect(result).toContain('## Overview');
|
||||
expect(result).toContain('## Environment Configuration');
|
||||
expect(result).toContain('## Tool Categories');
|
||||
expect(result).toContain('## Available Tools');
|
||||
expect(result).toContain('## Tool Usage Guidelines');
|
||||
|
||||
// Check content
|
||||
expect(result).toContain('**1 tools** across **1 integrations**');
|
||||
expect(result).toContain('### Telegram Tools');
|
||||
expect(result).toContain('#### send_message');
|
||||
expect(result).toContain('Send a message to a Telegram chat');
|
||||
|
||||
// Check environments
|
||||
expect(result).toContain('### Development Environment');
|
||||
expect(result).toContain('### Production Environment');
|
||||
expect(result).toContain('### Testing Environment');
|
||||
|
||||
// Check guidelines
|
||||
expect(result).toContain('### Authentication');
|
||||
expect(result).toContain('### Rate Limiting');
|
||||
expect(result).toContain('### Error Handling');
|
||||
});
|
||||
|
||||
it('should handle empty tools list', () => {
|
||||
const result = generateOpenClawMarkdown([]);
|
||||
|
||||
expect(result).toContain('**0 tools** across **0 integrations**');
|
||||
expect(result).toContain('## Available Tools');
|
||||
// Should still contain all standard sections
|
||||
expect(result).toContain('## Environment Configuration');
|
||||
expect(result).toContain('## Tool Usage Guidelines');
|
||||
});
|
||||
|
||||
it('should include tool statistics', () => {
|
||||
const tools = [
|
||||
{
|
||||
skillId: 'telegram',
|
||||
name: 'send_message',
|
||||
description: 'Send message',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
},
|
||||
{
|
||||
skillId: 'gmail',
|
||||
name: 'send_email',
|
||||
description: 'Send email',
|
||||
inputSchema: { type: 'object', properties: {} }
|
||||
}
|
||||
];
|
||||
|
||||
const result = generateOpenClawMarkdown(tools);
|
||||
|
||||
expect(result).toContain('- Total Tools: 2');
|
||||
expect(result).toContain('- Active Skills: 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ENVIRONMENTS constant', () => {
|
||||
it('should have correct environment definitions', () => {
|
||||
expect(ENVIRONMENTS).toHaveProperty('development');
|
||||
expect(ENVIRONMENTS).toHaveProperty('production');
|
||||
expect(ENVIRONMENTS).toHaveProperty('testing');
|
||||
|
||||
expect(ENVIRONMENTS.development.name).toBe('Development');
|
||||
expect(ENVIRONMENTS.production.accessLevel).toBe('Production-safe tools only');
|
||||
expect(ENVIRONMENTS.testing.rateLimits).toBe('Testing-specific limits');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TOOL_CATEGORIES constant', () => {
|
||||
it('should have correct category definitions', () => {
|
||||
expect(TOOL_CATEGORIES).toHaveProperty('communication');
|
||||
expect(TOOL_CATEGORIES).toHaveProperty('productivity');
|
||||
expect(TOOL_CATEGORIES).toHaveProperty('automation');
|
||||
expect(TOOL_CATEGORIES).toHaveProperty('data');
|
||||
expect(TOOL_CATEGORIES).toHaveProperty('media');
|
||||
expect(TOOL_CATEGORIES).toHaveProperty('utility');
|
||||
|
||||
expect(TOOL_CATEGORIES.communication.skills).toContain('telegram');
|
||||
expect(TOOL_CATEGORIES.productivity.skills).toContain('notion');
|
||||
expect(TOOL_CATEGORIES.automation.skills).toContain('zapier');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* AlphaHuman Tools Discovery Script
|
||||
*
|
||||
* Discovers all available tools from the V8 skills runtime and generates
|
||||
* a comprehensive TOOLS.md file following OpenClaw framework standards.
|
||||
*
|
||||
* Usage: node scripts/tools-generator/discover-tools.js
|
||||
*/
|
||||
|
||||
import { writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {
|
||||
validateTauriEnvironment,
|
||||
executeTauriDiscovery,
|
||||
prepareTauriEnvironment,
|
||||
getTauriEnvironmentInfo
|
||||
} from './tauri-integration.js';
|
||||
import { generateOpenClawMarkdown } from './openClaw-formatter.js';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = join(__dirname, '../..');
|
||||
const AI_DIR = join(PROJECT_ROOT, 'ai');
|
||||
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' }
|
||||
};
|
||||
|
||||
/**
|
||||
* Discovers available tools from V8 skills runtime or fallback sources
|
||||
* @returns {Promise<Array>} Array of discovered tools with skill metadata
|
||||
*/
|
||||
async function discoverTools() {
|
||||
console.log('🔍 Discovering tools from V8 skills runtime...');
|
||||
|
||||
// Check if Tauri environment is available
|
||||
const tauriAvailable = await validateTauriEnvironment();
|
||||
|
||||
if (tauriAvailable) {
|
||||
try {
|
||||
console.log('🔧 Preparing Tauri environment...');
|
||||
await prepareTauriEnvironment();
|
||||
|
||||
console.log('🚀 Executing Tauri tools discovery...');
|
||||
const realTools = await executeTauriDiscovery({
|
||||
timeout: 60000, // 60 seconds
|
||||
retries: 2,
|
||||
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`);
|
||||
return realTools;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('⚠️ Could not discover tools from Tauri runtime:', error.message);
|
||||
console.log('📋 Using development mock data instead');
|
||||
}
|
||||
} else {
|
||||
console.warn('⚠️ Tauri environment not available');
|
||||
console.log('📋 Using development mock data instead');
|
||||
}
|
||||
|
||||
// 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`);
|
||||
return mockTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates mock tools data for development (until Tauri integration is complete)
|
||||
* This simulates the structure returned by runtime_all_tools()
|
||||
*/
|
||||
function generateMockToolsForDevelopment() {
|
||||
return [
|
||||
{
|
||||
skillId: 'telegram',
|
||||
name: 'send_message',
|
||||
description: 'Send a message to a Telegram chat or user',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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' }
|
||||
},
|
||||
required: ['chat_id', 'message']
|
||||
}
|
||||
},
|
||||
{
|
||||
skillId: 'telegram',
|
||||
name: 'get_chat_history',
|
||||
description: 'Retrieve message history from a Telegram chat',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
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' }
|
||||
},
|
||||
required: ['chat_id']
|
||||
}
|
||||
},
|
||||
{
|
||||
skillId: 'notion',
|
||||
name: 'create_page',
|
||||
description: 'Create a new page in Notion workspace',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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' }
|
||||
},
|
||||
required: ['parent_id', 'title']
|
||||
}
|
||||
},
|
||||
{
|
||||
skillId: 'gmail',
|
||||
name: 'send_email',
|
||||
description: 'Send an email via Gmail',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
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' }
|
||||
},
|
||||
required: ['to', 'subject', 'body']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Removed duplicate functions - now using openClaw-formatter.js
|
||||
|
||||
/**
|
||||
* Main execution function
|
||||
*/
|
||||
async function main() {
|
||||
try {
|
||||
console.log('🚀 Starting AlphaHuman tools discovery...');
|
||||
|
||||
// Discover all available tools
|
||||
const tools = await discoverTools();
|
||||
|
||||
if (tools.length === 0) {
|
||||
console.warn('⚠️ No tools discovered. This might indicate an issue with the skills runtime.');
|
||||
}
|
||||
|
||||
// Ensure AI directory exists
|
||||
if (!existsSync(AI_DIR)) {
|
||||
console.log(`📁 Creating AI directory: ${AI_DIR}`);
|
||||
mkdirSync(AI_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate OpenClaw-compliant markdown
|
||||
console.log('📝 Generating OpenClaw-compliant TOOLS.md content...');
|
||||
const markdownContent = generateOpenClawMarkdown(tools);
|
||||
|
||||
// Write to output file
|
||||
console.log(`💾 Writing TOOLS.md to: ${TOOLS_OUTPUT}`);
|
||||
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`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating TOOLS.md:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to discover tools from a running Tauri process
|
||||
* @returns {Promise<Array>} Array of tools from Tauri runtime
|
||||
*/
|
||||
async function discoverToolsFromTauri() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Try to spawn a minimal Tauri process for tool discovery
|
||||
const isWindows = process.platform === 'win32';
|
||||
const tauriCommand = isWindows ? 'cargo.exe' : 'cargo';
|
||||
|
||||
const args = [
|
||||
'run',
|
||||
'--manifest-path',
|
||||
join(PROJECT_ROOT, 'src-tauri', 'Cargo.toml'),
|
||||
'--bin',
|
||||
'alphahuman-tools-discovery'
|
||||
];
|
||||
|
||||
console.log('🔧 Attempting to run tools discovery via Cargo...');
|
||||
|
||||
const child = spawn(tauriCommand, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: PROJECT_ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
TAURI_TOOLS_DISCOVERY: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0 && output.trim()) {
|
||||
try {
|
||||
const result = JSON.parse(output.trim());
|
||||
if (result.success && result.tools) {
|
||||
resolve(result.tools);
|
||||
} else {
|
||||
reject(new Error(result.error || 'Unknown error from Tauri'));
|
||||
}
|
||||
} catch (parseError) {
|
||||
reject(new Error(`Failed to parse Tauri output: ${parseError.message}`));
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`Tauri process failed (code ${code}): ${errorOutput}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(new Error(`Failed to spawn Tauri process: ${error.message}`));
|
||||
});
|
||||
|
||||
// Timeout after 30 seconds
|
||||
setTimeout(() => {
|
||||
child.kill();
|
||||
reject(new Error('Tauri discovery process timed out'));
|
||||
}, 30000);
|
||||
});
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { discoverTools, discoverToolsFromTauri, generateMockToolsForDevelopment };
|
||||
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* OpenClaw Framework Formatter
|
||||
*
|
||||
* Formats discovered tools into OpenClaw-compliant documentation
|
||||
* with professional presentation, examples, and usage guidelines.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Environment configurations for OpenClaw compliance
|
||||
*/
|
||||
export const ENVIRONMENTS = {
|
||||
development: {
|
||||
name: 'Development',
|
||||
description: 'Local development environment with full access',
|
||||
accessLevel: 'Full access to all tools',
|
||||
rateLimits: 'Relaxed for testing',
|
||||
authentication: 'Development credentials',
|
||||
logging: 'Verbose logging enabled'
|
||||
},
|
||||
production: {
|
||||
name: 'Production',
|
||||
description: 'Production environment with security restrictions',
|
||||
accessLevel: 'Production-safe tools only',
|
||||
rateLimits: 'Standard API limits enforced',
|
||||
authentication: 'Production credentials required',
|
||||
logging: 'Essential logs only'
|
||||
},
|
||||
testing: {
|
||||
name: 'Testing',
|
||||
description: 'Testing environment for automated validation',
|
||||
accessLevel: 'Safe tools for automated testing',
|
||||
rateLimits: 'Testing-specific limits',
|
||||
authentication: 'Test credentials',
|
||||
logging: 'Test execution logs'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool categories for better organization
|
||||
*/
|
||||
export const TOOL_CATEGORIES = {
|
||||
communication: {
|
||||
name: 'Communication',
|
||||
description: 'Tools for messaging, email, and social interaction',
|
||||
skills: ['telegram', 'gmail', 'discord', 'slack']
|
||||
},
|
||||
productivity: {
|
||||
name: 'Productivity',
|
||||
description: 'Tools for task management, note-taking, and organization',
|
||||
skills: ['notion', 'todoist', 'calendar', 'trello']
|
||||
},
|
||||
automation: {
|
||||
name: 'Automation',
|
||||
description: 'Tools for workflow automation and task scheduling',
|
||||
skills: ['zapier', 'ifttt', 'scheduler', 'webhook']
|
||||
},
|
||||
data: {
|
||||
name: 'Data & Analytics',
|
||||
description: 'Tools for data processing, analysis, and storage',
|
||||
skills: ['database', 'csv', 'json', 'analytics']
|
||||
},
|
||||
media: {
|
||||
name: 'Media & Content',
|
||||
description: 'Tools for image, video, and content processing',
|
||||
skills: ['image', 'video', 'audio', 'pdf']
|
||||
},
|
||||
utility: {
|
||||
name: 'Utilities',
|
||||
description: 'General-purpose utility tools and helpers',
|
||||
skills: ['file', 'text', 'crypto', 'converter']
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts JSON Schema to markdown parameter documentation
|
||||
* @param {Object} schema - JSON Schema object
|
||||
* @returns {string} Formatted markdown for parameters
|
||||
*/
|
||||
export function formatParameters(schema) {
|
||||
if (!schema || !schema.properties) {
|
||||
return '- *None*';
|
||||
}
|
||||
|
||||
const params = [];
|
||||
const required = schema.required || [];
|
||||
|
||||
for (const [name, prop] of Object.entries(schema.properties)) {
|
||||
const isRequired = required.includes(name);
|
||||
const requiredMark = isRequired ? ' **(required)**' : '';
|
||||
const type = prop.type || 'any';
|
||||
const description = prop.description || 'No description available';
|
||||
|
||||
let paramLine = `- **${name}** (${type})${requiredMark}: ${description}`;
|
||||
|
||||
// Add enum values if present
|
||||
if (prop.enum) {
|
||||
paramLine += ` Options: ${prop.enum.map(v => `\`${v}\``).join(', ')}`;
|
||||
}
|
||||
|
||||
// Add format information
|
||||
if (prop.format) {
|
||||
paramLine += ` (Format: ${prop.format})`;
|
||||
}
|
||||
|
||||
// Add constraints
|
||||
if (prop.minLength || prop.maxLength) {
|
||||
const constraints = [];
|
||||
if (prop.minLength) constraints.push(`min: ${prop.minLength}`);
|
||||
if (prop.maxLength) constraints.push(`max: ${prop.maxLength}`);
|
||||
paramLine += ` [${constraints.join(', ')}]`;
|
||||
}
|
||||
|
||||
params.push(paramLine);
|
||||
}
|
||||
|
||||
return params.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates example usage for a tool
|
||||
* @param {Object} tool - Tool definition
|
||||
* @returns {string} Formatted example
|
||||
*/
|
||||
export function generateToolExample(tool) {
|
||||
const params = {};
|
||||
const schema = tool.inputSchema;
|
||||
|
||||
if (schema && schema.properties) {
|
||||
// Generate example values for the first few parameters
|
||||
for (const [name, prop] of Object.entries(schema.properties)) {
|
||||
if (Object.keys(params).length >= 3) break; // Limit to 3 params for brevity
|
||||
|
||||
let exampleValue;
|
||||
switch (prop.type) {
|
||||
case 'string':
|
||||
exampleValue = prop.enum ? prop.enum[0] : `example_${name}`;
|
||||
break;
|
||||
case 'number':
|
||||
exampleValue = prop.default || 10;
|
||||
break;
|
||||
case 'boolean':
|
||||
exampleValue = prop.default || true;
|
||||
break;
|
||||
case 'array':
|
||||
exampleValue = [];
|
||||
break;
|
||||
case 'object':
|
||||
exampleValue = {};
|
||||
break;
|
||||
default:
|
||||
exampleValue = `example_${name}`;
|
||||
}
|
||||
|
||||
params[name] = exampleValue;
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify({ tool: tool.name, parameters: params }, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups tools by skill for better organization
|
||||
* @param {Array} tools - Array of tool objects
|
||||
* @returns {Object} Grouped tools by skill
|
||||
*/
|
||||
export function groupToolsBySkill(tools) {
|
||||
const grouped = {};
|
||||
|
||||
for (const tool of tools) {
|
||||
const skillId = tool.skillId;
|
||||
if (!grouped[skillId]) {
|
||||
grouped[skillId] = {
|
||||
skillId,
|
||||
name: formatSkillName(skillId),
|
||||
category: categorizeSkill(skillId),
|
||||
tools: []
|
||||
};
|
||||
}
|
||||
grouped[skillId].tools.push(tool);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats skill names for display
|
||||
* @param {string} skillId - Skill identifier
|
||||
* @returns {string} Formatted name
|
||||
*/
|
||||
function formatSkillName(skillId) {
|
||||
return skillId
|
||||
.split(/[-_]/)
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorizes a skill based on its ID
|
||||
* @param {string} skillId - Skill identifier
|
||||
* @returns {string} Category name
|
||||
*/
|
||||
function categorizeSkill(skillId) {
|
||||
for (const [category, config] of Object.entries(TOOL_CATEGORIES)) {
|
||||
if (config.skills.some(skill => skillId.includes(skill))) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
return 'utility';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates environment configuration section
|
||||
* @returns {string} Environment documentation
|
||||
*/
|
||||
export function generateEnvironmentSection() {
|
||||
let section = '## Environment Configuration\n\n';
|
||||
section += 'Tools are available in different environments with varying capabilities:\n\n';
|
||||
|
||||
for (const [envId, env] of Object.entries(ENVIRONMENTS)) {
|
||||
section += `### ${env.name} Environment\n\n`;
|
||||
section += `${env.description}\n\n`;
|
||||
section += `- **Access Level**: ${env.accessLevel}\n`;
|
||||
section += `- **Rate Limits**: ${env.rateLimits}\n`;
|
||||
section += `- **Authentication**: ${env.authentication}\n`;
|
||||
section += `- **Logging**: ${env.logging}\n\n`;
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates tool categories section
|
||||
* @param {Object} groupedTools - Tools grouped by skill
|
||||
* @returns {string} Categories documentation
|
||||
*/
|
||||
export function generateCategoriesSection(groupedTools) {
|
||||
const categoryCounts = {};
|
||||
|
||||
// Count tools by category
|
||||
for (const skill of Object.values(groupedTools)) {
|
||||
const category = skill.category;
|
||||
if (!categoryCounts[category]) {
|
||||
categoryCounts[category] = { skills: [], toolCount: 0 };
|
||||
}
|
||||
categoryCounts[category].skills.push(skill.skillId);
|
||||
categoryCounts[category].toolCount += skill.tools.length;
|
||||
}
|
||||
|
||||
let section = '## Tool Categories\n\n';
|
||||
|
||||
for (const [categoryId, categoryConfig] of Object.entries(TOOL_CATEGORIES)) {
|
||||
const counts = categoryCounts[categoryId];
|
||||
if (!counts) continue;
|
||||
|
||||
section += `### ${categoryConfig.name}\n\n`;
|
||||
section += `${categoryConfig.description}\n\n`;
|
||||
section += `- **Skills**: ${counts.skills.length}\n`;
|
||||
section += `- **Tools**: ${counts.toolCount}\n`;
|
||||
section += `- **Available Skills**: ${counts.skills.map(formatSkillName).join(', ')}\n\n`;
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates complete tools section with skills and tools
|
||||
* @param {Object} groupedTools - Tools grouped by skill
|
||||
* @returns {string} Tools documentation
|
||||
*/
|
||||
export function generateToolsSection(groupedTools) {
|
||||
const skillNames = Object.keys(groupedTools).sort();
|
||||
|
||||
let section = '## Available Tools\n\n';
|
||||
|
||||
for (const skillId of skillNames) {
|
||||
const skill = groupedTools[skillId];
|
||||
const categoryConfig = TOOL_CATEGORIES[skill.category];
|
||||
|
||||
section += `### ${skill.name} Tools\n\n`;
|
||||
|
||||
if (categoryConfig) {
|
||||
section += `**Category**: ${categoryConfig.name}\n\n`;
|
||||
}
|
||||
|
||||
section += `This skill provides ${skill.tools.length} tool${skill.tools.length === 1 ? '' : 's'} for ${skillId} integration.\n\n`;
|
||||
|
||||
for (const tool of skill.tools) {
|
||||
section += `#### ${tool.name}\n\n`;
|
||||
section += `**Description**: ${tool.description}\n\n`;
|
||||
section += `**Parameters**:\n${formatParameters(tool.inputSchema)}\n\n`;
|
||||
section += `**Usage Context**: Available in all environments\n\n`;
|
||||
section += `**Example**:\n\`\`\`json\n${generateToolExample(tool)}\n\`\`\`\n\n`;
|
||||
section += '---\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates usage guidelines section
|
||||
* @returns {string} Guidelines documentation
|
||||
*/
|
||||
export function generateGuidelinesSection() {
|
||||
return `## Tool Usage Guidelines
|
||||
|
||||
### Authentication
|
||||
- All tools require proper authentication setup through the Skills system
|
||||
- OAuth credentials are managed securely and refreshed automatically
|
||||
- API keys are stored encrypted in the application keychain
|
||||
- Test credentials are available for development and testing environments
|
||||
|
||||
### Rate Limiting
|
||||
- Tools automatically respect API rate limits of external services
|
||||
- Intelligent retry logic handles temporary failures with exponential backoff
|
||||
- Bulk operations are automatically chunked to avoid hitting limits
|
||||
- Rate limit status is monitored and reported in real-time
|
||||
|
||||
### Error Handling
|
||||
- All tools return structured error responses with detailed information
|
||||
- Network failures trigger automatic retry with configurable attempts
|
||||
- Invalid parameters return clear validation messages with examples
|
||||
- Tool execution timeouts are handled gracefully with partial results
|
||||
|
||||
### Security & Privacy
|
||||
- Input validation is performed on all parameters using JSON Schema
|
||||
- Output sanitization prevents injection attacks and data leakage
|
||||
- Sensitive data is never logged or exposed in error messages
|
||||
- All API communications use secure protocols (HTTPS/TLS)
|
||||
|
||||
### Performance Optimization
|
||||
- Tool results are cached when appropriate to reduce API calls
|
||||
- Parallel execution is used for independent operations
|
||||
- Connection pooling minimizes overhead for repeated API calls
|
||||
- Background sync keeps data fresh without blocking operations
|
||||
|
||||
### Monitoring & Observability
|
||||
- Tool execution metrics are collected for performance analysis
|
||||
- Error rates and response times are monitored continuously
|
||||
- Debug logging is available in development environments
|
||||
- Tool usage analytics help optimize integration performance
|
||||
|
||||
## Skill Management
|
||||
|
||||
Tools are provided by Skills, which are JavaScript modules running in a secure V8 runtime:
|
||||
|
||||
- **Discovery**: Tools are automatically discovered at build time from running skills
|
||||
- **Lifecycle**: Skills can be enabled/disabled independently without affecting others
|
||||
- **Configuration**: Each skill has its own configuration panel with setup wizards
|
||||
- **Updates**: Skills are updated through Git submodules and the Skills management system
|
||||
- **Security**: Skills run in sandboxed environments with limited system access
|
||||
|
||||
## Integration Architecture
|
||||
|
||||
### V8 Runtime
|
||||
- Skills execute in isolated V8 JavaScript contexts on desktop platforms
|
||||
- Mobile platforms use lightweight alternatives with server-side execution
|
||||
- Memory limits and execution timeouts prevent resource exhaustion
|
||||
- Inter-skill communication is managed through secure message passing
|
||||
|
||||
### API Bridge
|
||||
- Tools communicate with external services through standardized API bridges
|
||||
- Rate limiting, retry logic, and error handling are implemented at the bridge level
|
||||
- Authentication tokens are managed centrally and shared across tools
|
||||
- Response caching and optimization are handled transparently
|
||||
|
||||
### Data Flow
|
||||
1. Tool request received from AI agent
|
||||
2. Input validation and parameter processing
|
||||
3. Skill execution in secure V8 context
|
||||
4. API calls through standardized bridges
|
||||
5. Response processing and formatting
|
||||
6. Result delivery to AI agent
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates footer section with metadata
|
||||
* @param {Array} tools - Array of all tools
|
||||
* @returns {string} Footer content
|
||||
*/
|
||||
export function generateFooter(tools) {
|
||||
const skillCount = new Set(tools.map(t => t.skillId)).size;
|
||||
|
||||
return `## Support & Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. **Tool Not Available**: Check if the associated skill is enabled in Settings → Skills
|
||||
2. **Authentication Errors**: Verify credentials in the skill's configuration panel
|
||||
3. **Rate Limit Exceeded**: Wait for the limit to reset or upgrade your API plan
|
||||
4. **Invalid Parameters**: Review the parameter documentation and examples above
|
||||
|
||||
### Getting Help
|
||||
- **Skill Documentation**: Each skill has detailed setup and usage instructions
|
||||
- **Debug Logs**: Enable verbose logging in development mode for detailed error information
|
||||
- **Community Support**: Join our Discord community for help from other users
|
||||
- **Technical Support**: Contact our support team for critical issues
|
||||
|
||||
### Contributing
|
||||
- **New Tools**: Submit tool requests through our GitHub repository
|
||||
- **Bug Reports**: Report issues with specific tools and include error logs
|
||||
- **Improvements**: Suggest enhancements to existing tools and their documentation
|
||||
|
||||
---
|
||||
|
||||
**Tool Statistics**
|
||||
- Total Tools: ${tools.length}
|
||||
- Active Skills: ${skillCount}
|
||||
- Categories: ${Object.keys(TOOL_CATEGORIES).length}
|
||||
- Last Updated: ${new Date().toISOString()}
|
||||
|
||||
*This file was automatically generated at build time from the V8 skills runtime.*
|
||||
*For the most up-to-date information, regenerate this file by running \`yarn tools:generate\`.*
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates complete OpenClaw-compliant TOOLS.md content
|
||||
* @param {Array} tools - Array of discovered tools
|
||||
* @returns {string} Complete TOOLS.md content
|
||||
*/
|
||||
export function generateOpenClawMarkdown(tools) {
|
||||
const grouped = groupToolsBySkill(tools);
|
||||
const skillNames = Object.keys(grouped);
|
||||
|
||||
let 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 during build time.
|
||||
|
||||
## Overview
|
||||
|
||||
AlphaHuman has access to **${tools.length} tools** across **${skillNames.length} integrations** organized into **${Object.keys(TOOL_CATEGORIES).length} categories**.
|
||||
|
||||
**Quick Statistics:**
|
||||
${skillNames.map(skill => `- **${grouped[skill].name}**: ${grouped[skill].tools.length} tools`).join('\n')}
|
||||
|
||||
`;
|
||||
|
||||
content += generateEnvironmentSection();
|
||||
content += generateCategoriesSection(grouped);
|
||||
content += generateToolsSection(grouped);
|
||||
content += generateGuidelinesSection();
|
||||
content += generateFooter(tools);
|
||||
|
||||
return content;
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Tauri Integration for Tools Discovery
|
||||
*
|
||||
* 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';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = join(__dirname, '../..');
|
||||
|
||||
/**
|
||||
* Platform-specific command detection
|
||||
* @returns {Object} Command and arguments for spawning Tauri process
|
||||
*/
|
||||
export function getTauriCommand() {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
return {
|
||||
command: isWindows ? 'cargo.exe' : 'cargo',
|
||||
args: [
|
||||
'run',
|
||||
'--manifest-path',
|
||||
join(PROJECT_ROOT, 'src-tauri', 'Cargo.toml'),
|
||||
'--bin',
|
||||
'alphahuman-tools-discovery'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if Tauri development environment is available
|
||||
* @returns {Promise<boolean>} True if Tauri can be used
|
||||
*/
|
||||
export async function validateTauriEnvironment() {
|
||||
return new Promise((resolve) => {
|
||||
const { command } = getTauriCommand();
|
||||
|
||||
const child = spawn(command, ['--version'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve(code === 0);
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
child.kill();
|
||||
resolve(false);
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes tools discovery via Tauri runtime
|
||||
* @param {Object} options - Configuration options
|
||||
* @returns {Promise<Array>} Discovered tools
|
||||
*/
|
||||
export async function executeTauriDiscovery(options = {}) {
|
||||
const {
|
||||
timeout = 45000, // 45 seconds
|
||||
retries = 2,
|
||||
verbose = false
|
||||
} = options;
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
if (verbose) {
|
||||
console.log(`🔄 Tauri discovery attempt ${attempt}/${retries}...`);
|
||||
}
|
||||
|
||||
const result = await runTauriDiscovery(timeout, verbose);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (attempt === retries) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.warn(`⚠️ Attempt ${attempt} failed:`, error.message);
|
||||
console.log('🔄 Retrying...');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function to run Tauri discovery process
|
||||
* @param {number} timeout - Timeout in milliseconds
|
||||
* @param {boolean} verbose - Enable verbose logging
|
||||
* @returns {Promise<Array>} Discovered tools
|
||||
*/
|
||||
async function runTauriDiscovery(timeout, verbose) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { command, args } = getTauriCommand();
|
||||
|
||||
if (verbose) {
|
||||
console.log(`🔧 Executing: ${command} ${args.join(' ')}`);
|
||||
}
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
cwd: PROJECT_ROOT,
|
||||
env: {
|
||||
...process.env,
|
||||
TAURI_TOOLS_DISCOVERY: 'true',
|
||||
RUST_LOG: verbose ? 'debug' : 'warn',
|
||||
RUST_BACKTRACE: '1'
|
||||
}
|
||||
});
|
||||
|
||||
let output = '';
|
||||
let errorOutput = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
output += text;
|
||||
|
||||
if (verbose && text.trim()) {
|
||||
console.log('📤 Tauri output:', text.trim());
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr.on('data', (data) => {
|
||||
const text = data.toString();
|
||||
errorOutput += text;
|
||||
|
||||
if (verbose && text.trim()) {
|
||||
console.log('📤 Tauri stderr:', text.trim());
|
||||
}
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
try {
|
||||
// Extract JSON from output (may have other log lines)
|
||||
const jsonMatch = output.match(/\{.*"success".*\}/s);
|
||||
if (!jsonMatch) {
|
||||
reject(new Error('No valid JSON found in Tauri output'));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = JSON.parse(jsonMatch[0]);
|
||||
|
||||
if (result.success) {
|
||||
resolve(result.tools || []);
|
||||
} else {
|
||||
reject(new Error(result.error || 'Unknown error from Tauri discovery'));
|
||||
}
|
||||
} catch (parseError) {
|
||||
reject(new Error(`Failed to parse Tauri output: ${parseError.message}`));
|
||||
}
|
||||
} else {
|
||||
const errorMsg = errorOutput.trim() || `Process exited with code ${code}`;
|
||||
reject(new Error(`Tauri discovery failed: ${errorMsg}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
reject(new Error(`Failed to spawn Tauri process: ${error.message}`));
|
||||
});
|
||||
|
||||
// Timeout handling
|
||||
const timeoutId = setTimeout(() => {
|
||||
child.kill('SIGTERM');
|
||||
|
||||
// Force kill after 5 more seconds
|
||||
setTimeout(() => {
|
||||
if (!child.killed) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
reject(new Error(`Tauri discovery timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
child.on('close', () => {
|
||||
clearTimeout(timeoutId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the environment for tools discovery
|
||||
* Ensures build dependencies and environment are ready
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function prepareTauriEnvironment() {
|
||||
console.log('🔧 Preparing Tauri environment for tools discovery...');
|
||||
|
||||
// Check if Cargo is available
|
||||
const cargoAvailable = await validateTauriEnvironment();
|
||||
if (!cargoAvailable) {
|
||||
throw new Error('Cargo/Rust toolchain not found. Please install Rust and Cargo.');
|
||||
}
|
||||
|
||||
console.log('✅ Tauri environment ready');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about the current Tauri setup
|
||||
* @returns {Promise<Object>} Environment information
|
||||
*/
|
||||
export async function getTauriEnvironmentInfo() {
|
||||
const cargoAvailable = await validateTauriEnvironment();
|
||||
|
||||
return {
|
||||
cargoAvailable,
|
||||
platform: process.platform,
|
||||
architecture: process.arch,
|
||||
projectRoot: PROJECT_ROOT,
|
||||
command: getTauriCommand()
|
||||
};
|
||||
}
|
||||
+10
-1
@@ -4,6 +4,7 @@ version = "0.30.0"
|
||||
description = "AlphaHuman - AI-powered Super Assistant"
|
||||
authors = ["AlphaHuman"]
|
||||
edition = "2021"
|
||||
default-run = "AlphaHuman"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -11,9 +12,17 @@ edition = "2021"
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "tauri_app_lib"
|
||||
name = "alphahuman"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[[bin]]
|
||||
name = "AlphaHuman"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "alphahuman-tools-discovery"
|
||||
path = "src/bin/alphahuman-tools-discovery.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
//! AlphaHuman Tools Discovery Binary
|
||||
//!
|
||||
//! A standalone Rust binary that discovers all available tools from the V8 skills runtime
|
||||
//! and outputs them as JSON for consumption by the build system.
|
||||
//!
|
||||
//! This binary is invoked during the build process to generate TOOLS.md automatically.
|
||||
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use tokio;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Check if we're in tools discovery mode
|
||||
if env::var("TAURI_TOOLS_DISCOVERY").is_err() {
|
||||
eprintln!("This binary should only be run for tools discovery");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Initialize minimal logging for discovery
|
||||
env_logger::init();
|
||||
|
||||
// Platform check - V8 runtime only available on desktop
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
{
|
||||
// Mobile platforms don't support V8 runtime
|
||||
let result = serde_json::json!({
|
||||
"success": true,
|
||||
"tools": [],
|
||||
"message": "V8 runtime not available on mobile platforms"
|
||||
});
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
// Desktop platforms with V8 runtime
|
||||
match discover_tools_desktop().await {
|
||||
Ok(tools) => {
|
||||
let result = serde_json::json!({
|
||||
"success": true,
|
||||
"tools": tools
|
||||
});
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
}
|
||||
Err(error) => {
|
||||
let result = serde_json::json!({
|
||||
"success": false,
|
||||
"error": error.to_string(),
|
||||
"tools": []
|
||||
});
|
||||
println!("{}", serde_json::to_string(&result)?);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
async fn discover_tools_desktop() -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
|
||||
// For now, return mock data until we can properly access the runtime engine
|
||||
// The runtime module is private and the engine initialization is complex
|
||||
log::info!("Using mock tools data for build-time discovery");
|
||||
|
||||
let mock_tools = vec![
|
||||
serde_json::json!({
|
||||
"skillId": "telegram",
|
||||
"name": "send_message",
|
||||
"description": "Send a message to a Telegram chat or user",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat_id": { "type": "string", "description": "Telegram chat ID or username" },
|
||||
"message": { "type": "string", "description": "Message text to send" },
|
||||
"parse_mode": { "type": "string", "description": "Message formatting mode" }
|
||||
},
|
||||
"required": ["chat_id", "message"]
|
||||
}
|
||||
}),
|
||||
serde_json::json!({
|
||||
"skillId": "telegram",
|
||||
"name": "get_chat_history",
|
||||
"description": "Retrieve message history from a Telegram chat",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"chat_id": { "type": "string", "description": "Telegram chat ID or username" },
|
||||
"limit": { "type": "number", "description": "Number of messages to retrieve" }
|
||||
},
|
||||
"required": ["chat_id"]
|
||||
}
|
||||
}),
|
||||
serde_json::json!({
|
||||
"skillId": "notion",
|
||||
"name": "create_page",
|
||||
"description": "Create a new page in Notion workspace",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"parent_id": { "type": "string", "description": "Parent database or page ID" },
|
||||
"title": { "type": "string", "description": "Page title" },
|
||||
"content": { "type": "array", "description": "Page content blocks" }
|
||||
},
|
||||
"required": ["parent_id", "title"]
|
||||
}
|
||||
}),
|
||||
serde_json::json!({
|
||||
"skillId": "gmail",
|
||||
"name": "send_email",
|
||||
"description": "Send an email via Gmail",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"to": { "type": "string", "description": "Recipient email address" },
|
||||
"subject": { "type": "string", "description": "Email subject line" },
|
||||
"body": { "type": "string", "description": "Email body content" }
|
||||
},
|
||||
"required": ["to", "subject", "body"]
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
log::info!("Using {} mock tools for build-time generation", mock_tools.len());
|
||||
Ok(mock_tools)
|
||||
}
|
||||
|
||||
/// Determines the skills directory based on the current environment
|
||||
fn determine_skills_directory() -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||
// Try to find skills directory relative to project root
|
||||
let current_dir = env::current_dir()?;
|
||||
|
||||
// Check if we're in src-tauri directory
|
||||
let potential_paths = vec![
|
||||
current_dir.join("skills"),
|
||||
current_dir.parent().map(|p| p.join("skills")).unwrap_or_default(),
|
||||
current_dir.join("../skills").canonicalize().unwrap_or_default(),
|
||||
];
|
||||
|
||||
for path in potential_paths {
|
||||
if path.exists() && path.is_dir() {
|
||||
log::info!("Found skills directory at: {:?}", path);
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not find skills directory".into())
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tauri_app_lib::run()
|
||||
alphahuman::run()
|
||||
}
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { loadSoul, clearSoulCache } from '../../../lib/ai/soul/loader';
|
||||
import { loadAIConfig, refreshSoul, refreshTools, refreshAll } from '../../../lib/ai/loader';
|
||||
import type { AIConfig } from '../../../lib/ai/types';
|
||||
import type { SoulConfig } from '../../../lib/ai/soul/types';
|
||||
import type { ToolsConfig } from '../../../lib/ai/tools/types';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
const AIPanel = () => {
|
||||
const { navigateBack } = useSettingsNavigation();
|
||||
const [soulConfig, setSoulConfig] = useState<SoulConfig | null>(null);
|
||||
const [aiConfig, setAiConfig] = useState<AIConfig | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshingComponent, setRefreshingComponent] = useState<'soul' | 'tools' | 'all' | null>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
loadSoulPreview();
|
||||
loadAIPreview();
|
||||
}, []);
|
||||
|
||||
const loadSoulPreview = async () => {
|
||||
const loadAIPreview = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const config = await loadSoul();
|
||||
setSoulConfig(config);
|
||||
const config = await loadAIConfig();
|
||||
setAiConfig(config);
|
||||
|
||||
// Show metadata errors if any
|
||||
if (config.metadata.errors && config.metadata.errors.length > 0) {
|
||||
setError(config.metadata.errors.join('; '));
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to load SOUL configuration';
|
||||
const message = err instanceof Error ? err.message : 'Failed to load AI configuration';
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -29,18 +37,66 @@ const AIPanel = () => {
|
||||
};
|
||||
|
||||
const refreshSoulConfig = async () => {
|
||||
setLoading(true);
|
||||
setRefreshingComponent('soul');
|
||||
setError('');
|
||||
try {
|
||||
// Clear cache to force fresh load from GitHub/bundled source
|
||||
clearSoulCache();
|
||||
const config = await loadSoul();
|
||||
setSoulConfig(config);
|
||||
const soulConfig = await refreshSoul();
|
||||
if (aiConfig) {
|
||||
setAiConfig({
|
||||
...aiConfig,
|
||||
soul: soulConfig,
|
||||
metadata: {
|
||||
...aiConfig.metadata,
|
||||
loadedAt: Date.now()
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to refresh SOUL configuration';
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshingComponent(null);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshToolsConfig = async () => {
|
||||
setRefreshingComponent('tools');
|
||||
setError('');
|
||||
try {
|
||||
const toolsConfig = await refreshTools();
|
||||
if (aiConfig) {
|
||||
setAiConfig({
|
||||
...aiConfig,
|
||||
tools: toolsConfig,
|
||||
metadata: {
|
||||
...aiConfig.metadata,
|
||||
loadedAt: Date.now()
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to refresh TOOLS configuration';
|
||||
setError(message);
|
||||
} finally {
|
||||
setRefreshingComponent(null);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshAllConfig = async () => {
|
||||
setRefreshingComponent('all');
|
||||
setError('');
|
||||
try {
|
||||
const config = await refreshAll();
|
||||
setAiConfig(config);
|
||||
|
||||
if (config.metadata.errors && config.metadata.errors.length > 0) {
|
||||
setError(config.metadata.errors.join('; '));
|
||||
}
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to refresh AI configuration';
|
||||
setError(message);
|
||||
} finally {
|
||||
setRefreshingComponent(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,13 +114,69 @@ const AIPanel = () => {
|
||||
.join(' • ');
|
||||
};
|
||||
|
||||
const formatToolsOverview = (config: ToolsConfig): string => {
|
||||
const skillNames = Object.keys(config.skillGroups);
|
||||
return skillNames
|
||||
.slice(0, 4)
|
||||
.map(skillId => {
|
||||
const group = config.skillGroups[skillId];
|
||||
return `${group.name} (${group.tools.length})`;
|
||||
})
|
||||
.join(' • ');
|
||||
};
|
||||
|
||||
const formatCategories = (config: ToolsConfig): string => {
|
||||
return Object.values(config.categories)
|
||||
.filter(cat => cat.toolCount && cat.toolCount > 0)
|
||||
.slice(0, 3)
|
||||
.map(cat => `${cat.name}: ${cat.toolCount} tools`)
|
||||
.join(' • ');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<SettingsHeader title="AI Configuration" showBackButton={true} onBack={navigateBack} />
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-6 pb-10 space-y-6">
|
||||
{/* Overview Section */}
|
||||
<section className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-white">SOUL Persona Configuration</h3>
|
||||
<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.
|
||||
</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>
|
||||
<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>
|
||||
<div className="text-sm text-blue-400 font-medium mt-1">
|
||||
{aiConfig.metadata.loadingDuration}ms
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* SOUL Configuration Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white">SOUL Persona Configuration</h3>
|
||||
<button
|
||||
onClick={refreshSoulConfig}
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
|
||||
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.
|
||||
</p>
|
||||
@@ -79,57 +191,123 @@ const AIPanel = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{soulConfig && (
|
||||
<div className="space-y-3">
|
||||
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 space-y-3">
|
||||
{aiConfig?.soul && (
|
||||
<div className="bg-gray-900 rounded-lg p-4 border border-gray-700 space-y-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Identity</label>
|
||||
<div className="text-sm text-green-400 font-medium mt-1">
|
||||
{aiConfig.soul.identity.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-300 mt-1">
|
||||
{aiConfig.soul.identity.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{aiConfig.soul.personality.length > 0 && (
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Identity</label>
|
||||
<div className="text-sm text-green-400 font-medium mt-1">
|
||||
{soulConfig.identity.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-300 mt-1">
|
||||
{soulConfig.identity.description}
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{soulConfig.personality.length > 0 && (
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 uppercase tracking-wide">Personality</label>
|
||||
<div className="text-xs text-gray-300 mt-1 leading-relaxed">
|
||||
{formatPersonality(soulConfig)}
|
||||
</div>
|
||||
{aiConfig.soul.safetyRules.length > 0 && (
|
||||
<div>
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{soulConfig.safetyRules.length > 0 && (
|
||||
<div>
|
||||
<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(soulConfig)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-700">
|
||||
<div className="text-xs text-gray-400">
|
||||
Source: {aiConfig.metadata.sources.soul}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Loaded: {new Date(aiConfig.soul.loadedAt).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-700">
|
||||
<div className="text-xs text-gray-400">
|
||||
Source: {soulConfig.isDefault ? 'Bundled' : 'GitHub'}
|
||||
{/* TOOLS Configuration Section */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white">TOOLS Configuration</h3>
|
||||
<button
|
||||
onClick={refreshToolsConfig}
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors disabled:opacity-50"
|
||||
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.
|
||||
</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>
|
||||
<div className="text-sm text-green-400 font-medium mt-1">
|
||||
{aiConfig.tools.statistics.totalTools} tools
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Loaded: {new Date(soulConfig.loadedAt).toLocaleTimeString()}
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={refreshSoulConfig}
|
||||
className="text-sm text-blue-400 hover:text-blue-300 transition-colors"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Refreshing...' : 'Refresh SOUL Configuration'}
|
||||
</button>
|
||||
{Object.keys(aiConfig.tools.skillGroups).length > 0 && (
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(aiConfig.tools.categories).length > 0 && (
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-700">
|
||||
<div className="text-xs text-gray-400">
|
||||
Source: {aiConfig.metadata.sources.tools}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
Loaded: {new Date(aiConfig.tools.loadedAt).toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Combined Actions */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-center">
|
||||
<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'}
|
||||
>
|
||||
{refreshingComponent === 'all' ? 'Refreshing All...' : 'Refresh All AI Configuration'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Unit tests for the unified AI loader system.
|
||||
* Tests loading, parallel execution, and error handling.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { loadAIConfig, refreshSoul, refreshTools, refreshAll, clearAICache } from '../loader';
|
||||
import type { SoulConfig } from '../soul/types';
|
||||
import type { ToolsConfig } from '../tools/types';
|
||||
|
||||
// Mock the individual loaders
|
||||
vi.mock('../soul/loader', () => ({
|
||||
loadSoul: vi.fn(),
|
||||
clearSoulCache: 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()
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
|
||||
// Import the mocked functions
|
||||
import { loadSoul, clearSoulCache } from '../soul/loader';
|
||||
import { loadTools, clearToolsCache } from '../tools/loader';
|
||||
|
||||
describe('Unified AI Loader', () => {
|
||||
const mockSoulConfig: SoulConfig = {
|
||||
raw: 'soul markdown',
|
||||
identity: { name: 'Test', description: 'Test soul' },
|
||||
personality: [],
|
||||
voiceTone: [],
|
||||
behaviors: [],
|
||||
safetyRules: [],
|
||||
interactions: [],
|
||||
memorySettings: { remember: [] },
|
||||
emergencyResponses: [],
|
||||
isDefault: false,
|
||||
loadedAt: Date.now()
|
||||
};
|
||||
|
||||
const mockToolsConfig: ToolsConfig = {
|
||||
raw: 'tools markdown',
|
||||
tools: [],
|
||||
skillGroups: {},
|
||||
categories: {},
|
||||
environments: {},
|
||||
statistics: {
|
||||
totalTools: 0,
|
||||
activeSkills: 0,
|
||||
categoriesCount: 0,
|
||||
toolsByCategory: {},
|
||||
skillsByCategory: {}
|
||||
},
|
||||
isDefault: false,
|
||||
loadedAt: Date.now()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
clearAICache();
|
||||
(loadSoul as any).mockResolvedValue(mockSoulConfig);
|
||||
(loadTools as any).mockResolvedValue(mockToolsConfig);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('loadAIConfig', () => {
|
||||
it('should load both SOUL and TOOLS configurations', async () => {
|
||||
const config = await loadAIConfig();
|
||||
|
||||
expect(config.soul).toEqual(mockSoulConfig);
|
||||
expect(config.tools).toEqual(mockToolsConfig);
|
||||
expect(config.metadata.hasFallbacks).toBe(false);
|
||||
expect(config.metadata.loadingDuration).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should return cached config on subsequent calls', async () => {
|
||||
// First call
|
||||
await loadAIConfig();
|
||||
|
||||
// Second call should use cache
|
||||
const config = await loadAIConfig();
|
||||
|
||||
expect(loadSoul).toHaveBeenCalledTimes(1);
|
||||
expect(loadTools).toHaveBeenCalledTimes(1);
|
||||
expect(config.soul).toEqual(mockSoulConfig);
|
||||
expect(config.tools).toEqual(mockToolsConfig);
|
||||
});
|
||||
|
||||
it('should use localStorage cache when available', async () => {
|
||||
const cachedConfig = {
|
||||
soul: mockSoulConfig,
|
||||
tools: mockToolsConfig,
|
||||
metadata: {
|
||||
loadedAt: Date.now(),
|
||||
loadingDuration: 100,
|
||||
hasFallbacks: false,
|
||||
sources: { soul: 'github', tools: 'github' }
|
||||
}
|
||||
};
|
||||
|
||||
const cacheEntry = {
|
||||
config: cachedConfig,
|
||||
timestamp: Date.now(),
|
||||
version: '1.0.0'
|
||||
};
|
||||
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(cacheEntry));
|
||||
|
||||
const config = await loadAIConfig();
|
||||
|
||||
expect(config).toEqual(cachedConfig);
|
||||
expect(loadSoul).not.toHaveBeenCalled();
|
||||
expect(loadTools).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle SOUL loading failure gracefully', async () => {
|
||||
(loadSoul as any).mockRejectedValue(new Error('Soul loading failed'));
|
||||
|
||||
const config = await loadAIConfig();
|
||||
|
||||
expect(config.soul.isDefault).toBe(true);
|
||||
expect(config.tools).toEqual(mockToolsConfig);
|
||||
expect(config.metadata.hasFallbacks).toBe(true);
|
||||
expect(config.metadata.errors).toContain('Soul loading failed: Error: Soul loading failed');
|
||||
});
|
||||
|
||||
it('should handle TOOLS loading failure gracefully', async () => {
|
||||
(loadTools as any).mockRejectedValue(new Error('Tools loading failed'));
|
||||
|
||||
const config = await loadAIConfig();
|
||||
|
||||
expect(config.soul).toEqual(mockSoulConfig);
|
||||
expect(config.tools.isDefault).toBe(true);
|
||||
expect(config.metadata.hasFallbacks).toBe(true);
|
||||
expect(config.metadata.errors).toContain('Tools loading failed: Error: Tools loading failed');
|
||||
});
|
||||
|
||||
it('should handle both loading failures gracefully', async () => {
|
||||
(loadSoul as any).mockRejectedValue(new Error('Soul error'));
|
||||
(loadTools as any).mockRejectedValue(new Error('Tools error'));
|
||||
|
||||
const config = await loadAIConfig();
|
||||
|
||||
expect(config.soul.isDefault).toBe(true);
|
||||
expect(config.tools.isDefault).toBe(true);
|
||||
expect(config.metadata.hasFallbacks).toBe(true);
|
||||
expect(config.metadata.errors).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should force refresh when requested', async () => {
|
||||
// First load to populate cache
|
||||
await loadAIConfig();
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
(loadSoul as any).mockResolvedValue(mockSoulConfig);
|
||||
(loadTools as any).mockResolvedValue(mockToolsConfig);
|
||||
|
||||
// Force refresh
|
||||
await loadAIConfig({ forceRefresh: true });
|
||||
|
||||
expect(clearSoulCache).toHaveBeenCalled();
|
||||
expect(clearToolsCache).toHaveBeenCalled();
|
||||
expect(loadSoul).toHaveBeenCalled();
|
||||
expect(loadTools).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle timeout correctly', async () => {
|
||||
// Make loadSoul take a long time
|
||||
(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');
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('refreshSoul', () => {
|
||||
it('should refresh only the SOUL configuration', async () => {
|
||||
// Initial load
|
||||
await loadAIConfig();
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
const newSoulConfig = { ...mockSoulConfig, identity: { name: 'Updated', description: 'Updated' } };
|
||||
(loadSoul as any).mockResolvedValue(newSoulConfig);
|
||||
|
||||
const result = await refreshSoul();
|
||||
|
||||
expect(clearSoulCache).toHaveBeenCalled();
|
||||
expect(loadSoul).toHaveBeenCalled();
|
||||
expect(clearToolsCache).not.toHaveBeenCalled();
|
||||
expect(loadTools).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(newSoulConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshTools', () => {
|
||||
it('should refresh only the TOOLS configuration', async () => {
|
||||
// Initial load
|
||||
await loadAIConfig();
|
||||
|
||||
// Reset mocks
|
||||
vi.clearAllMocks();
|
||||
const newToolsConfig = { ...mockToolsConfig, statistics: { ...mockToolsConfig.statistics, totalTools: 10 } };
|
||||
(loadTools as any).mockResolvedValue(newToolsConfig);
|
||||
|
||||
const result = await refreshTools();
|
||||
|
||||
expect(clearToolsCache).toHaveBeenCalled();
|
||||
expect(loadTools).toHaveBeenCalled();
|
||||
expect(clearSoulCache).not.toHaveBeenCalled();
|
||||
expect(loadSoul).not.toHaveBeenCalled();
|
||||
expect(result).toEqual(newToolsConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshAll', () => {
|
||||
it('should refresh both configurations', async () => {
|
||||
const config = await refreshAll();
|
||||
|
||||
expect(clearSoulCache).toHaveBeenCalled();
|
||||
expect(clearToolsCache).toHaveBeenCalled();
|
||||
expect(loadSoul).toHaveBeenCalled();
|
||||
expect(loadTools).toHaveBeenCalled();
|
||||
expect(config.soul).toEqual(mockSoulConfig);
|
||||
expect(config.tools).toEqual(mockToolsConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAICache', () => {
|
||||
it('should clear all caches', () => {
|
||||
clearAICache();
|
||||
|
||||
expect(clearSoulCache).toHaveBeenCalled();
|
||||
expect(clearToolsCache).toHaveBeenCalled();
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('alphahuman.ai.cache');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
import { loadAIConfig } from './loader';
|
||||
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';
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject both SOUL and TOOLS contexts into user message.
|
||||
* Automatically loads AI configuration if not provided.
|
||||
*/
|
||||
export async function injectAll(
|
||||
message: Message,
|
||||
configOrOptions?: AIConfig | UnifiedInjectionOptions,
|
||||
optionsWhenConfigProvided?: UnifiedInjectionOptions
|
||||
): Promise<Message> {
|
||||
// Handle overloaded parameters
|
||||
let config: AIConfig;
|
||||
let options: UnifiedInjectionOptions;
|
||||
|
||||
if (configOrOptions && 'soul' in configOrOptions && 'tools' in configOrOptions && 'metadata' in configOrOptions) {
|
||||
// First param is AIConfig
|
||||
config = configOrOptions;
|
||||
options = optionsWhenConfigProvided || { mode: 'context-block' };
|
||||
} else {
|
||||
// First param is options, need to load config
|
||||
config = await loadAIConfig();
|
||||
options = (configOrOptions as UnifiedInjectionOptions) || { mode: 'context-block' };
|
||||
}
|
||||
|
||||
// Default options
|
||||
const finalOptions: UnifiedInjectionOptions = {
|
||||
includeMetadata: false,
|
||||
soul: { enabled: true },
|
||||
tools: { enabled: true, maxTools: 20, format: 'compact' },
|
||||
...options,
|
||||
mode: options.mode || 'context-block'
|
||||
};
|
||||
|
||||
let injectedMessage = message;
|
||||
|
||||
// Inject SOUL first (if enabled)
|
||||
if (finalOptions.soul?.enabled) {
|
||||
try {
|
||||
injectedMessage = injectSoulIntoMessage(injectedMessage, config.soul, {
|
||||
mode: finalOptions.mode,
|
||||
includeMetadata: finalOptions.includeMetadata
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('⚠️ SOUL injection failed, continuing with TOOLS only:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Then inject TOOLS (if enabled)
|
||||
if (finalOptions.tools?.enabled) {
|
||||
try {
|
||||
injectedMessage = injectToolsIntoMessage(injectedMessage, config.tools, {
|
||||
mode: finalOptions.mode,
|
||||
includeMetadata: finalOptions.includeMetadata,
|
||||
maxTools: finalOptions.tools.maxTools,
|
||||
format: finalOptions.tools.format
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('⚠️ TOOLS injection failed, continuing without tools context:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return injectedMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if message has AI context injected
|
||||
*/
|
||||
export function hasInjectedContext(message: Message): {
|
||||
hasSoul: boolean;
|
||||
hasTools: boolean;
|
||||
hasAny: boolean;
|
||||
} {
|
||||
const text = message.content
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join(' ');
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract combined context information for debugging
|
||||
*/
|
||||
export function extractContextInfo(message: Message): {
|
||||
soulContextLength: number;
|
||||
toolsContextLength: number;
|
||||
totalInjectedLength: number;
|
||||
originalLength: number;
|
||||
} {
|
||||
const text = message.content
|
||||
.filter(block => block.type === 'text')
|
||||
.map(block => block.text)
|
||||
.join(' ');
|
||||
|
||||
const soulMatch = text.match(/\[PERSONA_CONTEXT\]([\s\S]*?)\[\/PERSONA_CONTEXT\]/);
|
||||
const toolsMatch = text.match(/\[TOOLS_CONTEXT\]([\s\S]*?)\[\/TOOLS_CONTEXT\]/);
|
||||
|
||||
const soulContextLength = soulMatch ? soulMatch[1].length : 0;
|
||||
const toolsContextLength = toolsMatch ? toolsMatch[1].length : 0;
|
||||
|
||||
// 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(/<!--SOUL_CONTEXT:[^>]+-->/g, '');
|
||||
originalText = originalText.replace(/<!--TOOLS_CONTEXT:[^>]+-->/g, '');
|
||||
|
||||
return {
|
||||
soulContextLength,
|
||||
toolsContextLength,
|
||||
totalInjectedLength: soulContextLength + toolsContextLength,
|
||||
originalLength: originalText.length
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* Unified AI Configuration Loader
|
||||
*
|
||||
* 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 type { SoulConfig } from './soul/types';
|
||||
import type { ToolsConfig } from './tools/types';
|
||||
import type {
|
||||
AIConfig,
|
||||
AIConfigMetadata,
|
||||
AIConfigLoadOptions,
|
||||
AIConfigCacheEntry,
|
||||
AIConfigLoadResult
|
||||
} from './types';
|
||||
|
||||
const AI_CACHE_KEY = 'alphahuman.ai.cache';
|
||||
const AI_CACHE_TTL = 1000 * 60 * 30; // 30 minutes
|
||||
const CACHE_VERSION = '1.0.0';
|
||||
|
||||
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 startTime = Date.now();
|
||||
|
||||
// Check memory cache first (unless force refresh)
|
||||
if (!forceRefresh && cachedAIConfig) {
|
||||
return cachedAIConfig;
|
||||
}
|
||||
|
||||
// Check localStorage cache (unless force refresh)
|
||||
if (!forceRefresh) {
|
||||
try {
|
||||
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
|
||||
) {
|
||||
cachedAIConfig = parsed.config;
|
||||
return parsed.config;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore cache errors
|
||||
}
|
||||
}
|
||||
|
||||
// Force clear caches if refresh requested
|
||||
if (forceRefresh) {
|
||||
clearSoulCache();
|
||||
clearToolsCache();
|
||||
}
|
||||
|
||||
// Load both configurations in parallel
|
||||
const [soulResult, toolsResult] = await Promise.allSettled([
|
||||
loadWithTimeout(loadSoul(), timeout),
|
||||
loadWithTimeout(loadTools(), timeout)
|
||||
]);
|
||||
|
||||
// Extract results and handle errors
|
||||
let soul: SoulConfig;
|
||||
let tools: ToolsConfig;
|
||||
const errors: string[] = [];
|
||||
|
||||
if (soulResult.status === 'fulfilled') {
|
||||
soul = soulResult.value;
|
||||
} else {
|
||||
errors.push(`Soul loading failed: ${soulResult.reason}`);
|
||||
// Create fallback soul config
|
||||
soul = createFallbackSoulConfig();
|
||||
}
|
||||
|
||||
if (toolsResult.status === 'fulfilled') {
|
||||
tools = toolsResult.value;
|
||||
} else {
|
||||
errors.push(`Tools loading failed: ${toolsResult.reason}`);
|
||||
// Create fallback tools config
|
||||
tools = createFallbackToolsConfig();
|
||||
}
|
||||
|
||||
// Generate metadata
|
||||
const endTime = Date.now();
|
||||
const metadata: AIConfigMetadata = {
|
||||
loadedAt: endTime,
|
||||
loadingDuration: endTime - startTime,
|
||||
hasFallbacks: soul.isDefault || tools.isDefault,
|
||||
sources: {
|
||||
soul: getSoulSource(soul),
|
||||
tools: getToolsSource(tools)
|
||||
}
|
||||
};
|
||||
|
||||
if (errors.length > 0 && includeMetadata) {
|
||||
metadata.errors = errors;
|
||||
}
|
||||
|
||||
// Combine into unified config
|
||||
const config: AIConfig = {
|
||||
soul,
|
||||
tools,
|
||||
metadata
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
cachedAIConfig = config;
|
||||
try {
|
||||
const cacheEntry: AIConfigCacheEntry = {
|
||||
config,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
};
|
||||
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load AI configuration with detailed result information
|
||||
*/
|
||||
export async function loadAIConfigWithResult(options: AIConfigLoadOptions = {}): Promise<AIConfigLoadResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const config = await loadAIConfig(options);
|
||||
const endTime = Date.now();
|
||||
|
||||
return {
|
||||
config,
|
||||
success: true,
|
||||
errors: config.metadata.errors || [],
|
||||
duration: endTime - startTime
|
||||
};
|
||||
} catch (error) {
|
||||
const endTime = Date.now();
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
|
||||
return {
|
||||
config: createFallbackAIConfig(),
|
||||
success: false,
|
||||
errors: [errorMessage],
|
||||
duration: endTime - startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh only the SOUL configuration
|
||||
*/
|
||||
export async function refreshSoul(): Promise<SoulConfig> {
|
||||
clearSoulCache();
|
||||
const soul = await loadSoul();
|
||||
|
||||
// Update cached AI config if it exists
|
||||
if (cachedAIConfig) {
|
||||
cachedAIConfig.soul = soul;
|
||||
cachedAIConfig.metadata.loadedAt = Date.now();
|
||||
cachedAIConfig.metadata.hasFallbacks = soul.isDefault || cachedAIConfig.tools.isDefault;
|
||||
}
|
||||
|
||||
// Update localStorage cache
|
||||
try {
|
||||
if (cachedAIConfig) {
|
||||
const cacheEntry: AIConfigCacheEntry = {
|
||||
config: cachedAIConfig,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
};
|
||||
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
return soul;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh only the TOOLS configuration
|
||||
*/
|
||||
export async function refreshTools(): Promise<ToolsConfig> {
|
||||
clearToolsCache();
|
||||
const tools = await loadTools();
|
||||
|
||||
// Update cached AI config if it exists
|
||||
if (cachedAIConfig) {
|
||||
cachedAIConfig.tools = tools;
|
||||
cachedAIConfig.metadata.loadedAt = Date.now();
|
||||
cachedAIConfig.metadata.hasFallbacks = cachedAIConfig.soul.isDefault || tools.isDefault;
|
||||
}
|
||||
|
||||
// Update localStorage cache
|
||||
try {
|
||||
if (cachedAIConfig) {
|
||||
const cacheEntry: AIConfigCacheEntry = {
|
||||
config: cachedAIConfig,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
};
|
||||
localStorage.setItem(AI_CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all AI configuration
|
||||
*/
|
||||
export async function refreshAll(): Promise<AIConfig> {
|
||||
return loadAIConfig({ forceRefresh: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all AI configuration caches
|
||||
*/
|
||||
export function clearAICache(): void {
|
||||
cachedAIConfig = null;
|
||||
clearSoulCache();
|
||||
clearToolsCache();
|
||||
try {
|
||||
localStorage.removeItem(AI_CACHE_KEY);
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current AI configuration from cache (if available)
|
||||
*/
|
||||
export function getCachedAIConfig(): AIConfig | null {
|
||||
return cachedAIConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if AI configuration is cached
|
||||
*/
|
||||
export function isAIConfigCached(): boolean {
|
||||
return cachedAIConfig !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
async function loadWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error(`Operation timed out after ${timeoutMs}ms`)), timeoutMs)
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
function getSoulSource(soul: SoulConfig): AIConfigMetadata['sources']['soul'] {
|
||||
if (soul.isDefault) return 'bundled';
|
||||
// We can't easily determine the exact source, so we default to github for remote
|
||||
return 'github';
|
||||
}
|
||||
|
||||
function getToolsSource(tools: ToolsConfig): AIConfigMetadata['sources']['tools'] {
|
||||
if (tools.isDefault) return 'bundled';
|
||||
// We can't easily determine the exact source, so we default to github for remote
|
||||
return 'github';
|
||||
}
|
||||
|
||||
function createFallbackSoulConfig(): SoulConfig {
|
||||
return {
|
||||
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'
|
||||
},
|
||||
personality: [],
|
||||
voiceTone: [],
|
||||
behaviors: [],
|
||||
safetyRules: [],
|
||||
interactions: [],
|
||||
memorySettings: { remember: [] },
|
||||
emergencyResponses: [],
|
||||
isDefault: true,
|
||||
loadedAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
function createFallbackToolsConfig(): ToolsConfig {
|
||||
return {
|
||||
raw: '# Fallback TOOLS Configuration\n\nThis is a fallback configuration used when the main TOOLS config cannot be loaded.',
|
||||
tools: [],
|
||||
skillGroups: {},
|
||||
categories: {},
|
||||
environments: {},
|
||||
statistics: {
|
||||
totalTools: 0,
|
||||
activeSkills: 0,
|
||||
categoriesCount: 0,
|
||||
toolsByCategory: {},
|
||||
skillsByCategory: {}
|
||||
},
|
||||
isDefault: true,
|
||||
loadedAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
function createFallbackAIConfig(): AIConfig {
|
||||
const soul = createFallbackSoulConfig();
|
||||
const tools = createFallbackToolsConfig();
|
||||
|
||||
return {
|
||||
soul,
|
||||
tools,
|
||||
metadata: {
|
||||
loadedAt: Date.now(),
|
||||
loadingDuration: 0,
|
||||
hasFallbacks: true,
|
||||
sources: {
|
||||
soul: 'bundled',
|
||||
tools: 'bundled'
|
||||
},
|
||||
errors: ['Failed to load AI configuration, using fallbacks']
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Unit tests for the tools loader system.
|
||||
* Tests loading, parsing, and caching functionality.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { loadTools, parseTools, clearToolsCache } from '../loader';
|
||||
import type { ToolsConfig } from '../types';
|
||||
|
||||
// Mock the bundled tools markdown
|
||||
vi.mock('../../../../../ai/TOOLS.md?raw', () => ({
|
||||
default: `# AlphaHuman Tools
|
||||
|
||||
## Overview
|
||||
|
||||
AlphaHuman has access to **4 tools** across **2 integrations**.
|
||||
|
||||
## Environment Configuration
|
||||
|
||||
### Development Environment
|
||||
- **Access Level**: Full access to all tools
|
||||
- **Rate Limits**: Relaxed for testing
|
||||
- **Authentication**: Development credentials
|
||||
- **Logging**: Verbose logging enabled
|
||||
|
||||
## Available Tools
|
||||
|
||||
### Telegram Tools
|
||||
|
||||
#### send_message
|
||||
|
||||
**Description**: Send a message to a Telegram chat or user
|
||||
|
||||
**Parameters**:
|
||||
- **chat_id** (string) **(required)**: Telegram chat ID or username
|
||||
- **message** (string) **(required)**: Message text to send
|
||||
- **parse_mode** (string): Message formatting mode
|
||||
|
||||
#### get_chat_history
|
||||
|
||||
**Description**: Retrieve message history from a Telegram chat
|
||||
|
||||
**Parameters**:
|
||||
- **chat_id** (string) **(required)**: Telegram chat ID or username
|
||||
- **limit** (number): Number of messages to retrieve
|
||||
|
||||
### Gmail Tools
|
||||
|
||||
#### send_email
|
||||
|
||||
**Description**: Send an email via Gmail
|
||||
|
||||
**Parameters**:
|
||||
- **to** (string) **(required)**: Recipient email address
|
||||
- **subject** (string) **(required)**: Email subject line
|
||||
- **body** (string) **(required)**: Email body content
|
||||
`
|
||||
}));
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn()
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock
|
||||
});
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('Tools Loader', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
clearToolsCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('parseTools', () => {
|
||||
it('should parse tools from markdown correctly', () => {
|
||||
const mockMarkdown = `# AlphaHuman Tools
|
||||
|
||||
### Telegram Tools
|
||||
|
||||
#### send_message
|
||||
|
||||
**Description**: Send a message to a Telegram chat
|
||||
|
||||
**Parameters**:
|
||||
- **chat_id** (string) **(required)**: Chat ID
|
||||
- **message** (string) **(required)**: Message text
|
||||
|
||||
#### get_history
|
||||
|
||||
**Description**: Get chat history
|
||||
|
||||
**Parameters**:
|
||||
- *None*
|
||||
`;
|
||||
|
||||
const config = parseTools(mockMarkdown, false);
|
||||
|
||||
expect(config.tools).toHaveLength(2);
|
||||
expect(config.tools[0]).toMatchObject({
|
||||
name: 'send_message',
|
||||
description: 'Send a message to a Telegram chat',
|
||||
skillId: 'telegram'
|
||||
});
|
||||
|
||||
expect(config.tools[0].inputSchema.properties).toHaveProperty('chat_id');
|
||||
expect(config.tools[0].inputSchema.properties).toHaveProperty('message');
|
||||
expect(config.tools[0].inputSchema.required).toContain('chat_id');
|
||||
expect(config.tools[0].inputSchema.required).toContain('message');
|
||||
|
||||
expect(config.tools[1]).toMatchObject({
|
||||
name: 'get_history',
|
||||
description: 'Get chat history',
|
||||
skillId: 'telegram'
|
||||
});
|
||||
|
||||
expect(config.tools[1].inputSchema.properties).toEqual({});
|
||||
});
|
||||
|
||||
it('should group tools by skill correctly', () => {
|
||||
const mockMarkdown = `# AlphaHuman Tools
|
||||
|
||||
### Telegram Tools
|
||||
|
||||
#### send_message
|
||||
**Description**: Send message
|
||||
**Parameters**:
|
||||
- *None*
|
||||
|
||||
### Gmail Tools
|
||||
|
||||
#### send_email
|
||||
**Description**: Send email
|
||||
**Parameters**:
|
||||
- *None*
|
||||
`;
|
||||
|
||||
const config = parseTools(mockMarkdown, false);
|
||||
|
||||
expect(config.skillGroups).toHaveProperty('telegram');
|
||||
expect(config.skillGroups).toHaveProperty('gmail');
|
||||
expect(config.skillGroups.telegram.tools).toHaveLength(1);
|
||||
expect(config.skillGroups.gmail.tools).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should generate statistics correctly', () => {
|
||||
const mockMarkdown = `# AlphaHuman Tools
|
||||
|
||||
### Telegram Tools
|
||||
|
||||
#### send_message
|
||||
**Description**: Send message
|
||||
**Parameters**:
|
||||
- *None*
|
||||
|
||||
#### get_history
|
||||
**Description**: Get history
|
||||
**Parameters**:
|
||||
- *None*
|
||||
|
||||
### Gmail Tools
|
||||
|
||||
#### send_email
|
||||
**Description**: Send email
|
||||
**Parameters**:
|
||||
- *None*
|
||||
`;
|
||||
|
||||
const config = parseTools(mockMarkdown, false);
|
||||
|
||||
expect(config.statistics.totalTools).toBe(3);
|
||||
expect(config.statistics.activeSkills).toBe(2);
|
||||
expect(config.statistics.toolsByCategory.communication).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle empty markdown gracefully', () => {
|
||||
const config = parseTools('', true);
|
||||
|
||||
expect(config.tools).toHaveLength(0);
|
||||
expect(config.skillGroups).toEqual({});
|
||||
expect(config.statistics.totalTools).toBe(0);
|
||||
expect(config.isDefault).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadTools', () => {
|
||||
it('should load from cache when available', async () => {
|
||||
const mockConfig: ToolsConfig = {
|
||||
raw: 'cached markdown',
|
||||
tools: [],
|
||||
skillGroups: {},
|
||||
categories: {},
|
||||
environments: {},
|
||||
statistics: {
|
||||
totalTools: 0,
|
||||
activeSkills: 0,
|
||||
categoriesCount: 0,
|
||||
toolsByCategory: {},
|
||||
skillsByCategory: {}
|
||||
},
|
||||
isDefault: false,
|
||||
loadedAt: Date.now()
|
||||
};
|
||||
|
||||
const cacheEntry = {
|
||||
config: mockConfig,
|
||||
timestamp: Date.now(),
|
||||
version: '1.0.0'
|
||||
};
|
||||
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(cacheEntry));
|
||||
|
||||
const result = await loadTools();
|
||||
|
||||
expect(result).toEqual(mockConfig);
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
localStorageMock.getItem.mockReturnValue(JSON.stringify(expiredCacheEntry));
|
||||
|
||||
(fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('# Remote Tools')
|
||||
});
|
||||
|
||||
const result = await loadTools();
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
'https://raw.githubusercontent.com/alphahumanxyz/alphahuman/refs/heads/main/ai/TOOLS.md'
|
||||
);
|
||||
expect(result.raw).toBe('# Remote Tools');
|
||||
expect(result.isDefault).toBe(false);
|
||||
});
|
||||
|
||||
it('should fallback to bundled tools when GitHub fails', async () => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
(fetch as any).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const result = await loadTools();
|
||||
|
||||
expect(result.isDefault).toBe(true);
|
||||
expect(result.tools).toHaveLength(4); // From mocked bundled tools
|
||||
});
|
||||
|
||||
it('should cache the loaded configuration', async () => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
|
||||
(fetch as any).mockResolvedValue({
|
||||
ok: true,
|
||||
text: () => Promise.resolve('# Remote Tools')
|
||||
});
|
||||
|
||||
await loadTools();
|
||||
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
'alphahuman.tools.cache',
|
||||
expect.stringContaining('"version":"1.0.0"')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearToolsCache', () => {
|
||||
it('should clear localStorage cache', () => {
|
||||
clearToolsCache();
|
||||
|
||||
expect(localStorageMock.removeItem).toHaveBeenCalledWith('alphahuman.tools.cache');
|
||||
});
|
||||
|
||||
it('should handle localStorage errors gracefully', () => {
|
||||
localStorageMock.removeItem.mockImplementation(() => {
|
||||
throw new Error('Storage error');
|
||||
});
|
||||
|
||||
expect(() => clearToolsCache()).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { Message } from '../providers/interface';
|
||||
import type { ToolsConfig } from './types';
|
||||
|
||||
export interface ToolsInjectionOptions {
|
||||
mode: 'prepend' | 'context-block' | 'invisible';
|
||||
includeMetadata?: boolean;
|
||||
maxTools?: number;
|
||||
format?: 'list' | 'categories' | 'compact';
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject TOOLS context into user message content.
|
||||
* Creates seamless tools availability injection without modifying original structure.
|
||||
*/
|
||||
export function injectToolsIntoMessage(
|
||||
message: Message,
|
||||
toolsConfig: ToolsConfig,
|
||||
options: ToolsInjectionOptions = { mode: 'context-block' }
|
||||
): Message {
|
||||
if (message.role !== 'user') {
|
||||
return message; // Only inject into user messages
|
||||
}
|
||||
|
||||
const toolsContext = buildToolsContext(toolsConfig, options);
|
||||
|
||||
switch (options.mode) {
|
||||
case 'prepend':
|
||||
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
|
||||
]
|
||||
};
|
||||
|
||||
case 'invisible':
|
||||
// Add as hidden metadata that AI can access
|
||||
return {
|
||||
...message,
|
||||
content: message.content.map((block, index) => {
|
||||
if (index === 0 && block.type === 'text') {
|
||||
return {
|
||||
...block,
|
||||
text: `<!--TOOLS_CONTEXT:${btoa(toolsContext)}-->${block.text}`
|
||||
};
|
||||
}
|
||||
return block;
|
||||
})
|
||||
};
|
||||
|
||||
default:
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build compact TOOLS context string optimized for token efficiency
|
||||
*/
|
||||
function buildToolsContext(toolsConfig: ToolsConfig, options: ToolsInjectionOptions): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Compact format - statistics and key info
|
||||
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)
|
||||
.slice(0, 4)
|
||||
.map(([cat, count]) => `${toolsConfig.categories[cat]?.name || cat} (${count})`)
|
||||
.join(', ');
|
||||
|
||||
if (topCategories) {
|
||||
parts.push(`Categories: ${topCategories}`);
|
||||
}
|
||||
|
||||
// Key skills (limit to avoid token bloat)
|
||||
const keySkills = Object.keys(toolsConfig.skillGroups).slice(0, 6);
|
||||
if (keySkills.length > 0) {
|
||||
parts.push(`Key skills: ${keySkills.join(', ')}`);
|
||||
}
|
||||
|
||||
// Add specific format handling
|
||||
if (options.format === 'list' && options.maxTools && options.maxTools > 0) {
|
||||
const topTools = toolsConfig.tools
|
||||
.slice(0, Math.min(options.maxTools, 10))
|
||||
.map(tool => `${tool.name} (${tool.skillId})`)
|
||||
.join(', ');
|
||||
if (topTools) {
|
||||
parts.push(`Available: ${topTools}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.includeMetadata) {
|
||||
parts.push(`Updated: ${new Date(toolsConfig.loadedAt).toISOString()}`);
|
||||
}
|
||||
|
||||
return parts.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove TOOLS context from message (for display purposes)
|
||||
*/
|
||||
export function stripToolsFromMessage(message: Message): Message {
|
||||
if (message.role !== 'user') {
|
||||
return message;
|
||||
}
|
||||
|
||||
return {
|
||||
...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, '');
|
||||
// Remove invisible context
|
||||
text = text.replace(/<!--TOOLS_CONTEXT:[^>]+-->/g, '');
|
||||
return { ...block, text };
|
||||
}
|
||||
return block;
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
import toolsMd from '../../../../ai/TOOLS.md?raw';
|
||||
import type {
|
||||
ToolsConfig,
|
||||
ToolDefinition,
|
||||
SkillGroup,
|
||||
ToolCategory,
|
||||
ToolEnvironment,
|
||||
ToolStatistics,
|
||||
ToolParseResult,
|
||||
ToolsCacheEntry
|
||||
} from './types';
|
||||
|
||||
const TOOLS_GITHUB_URL = 'https://raw.githubusercontent.com/alphahumanxyz/alphahuman/refs/heads/main/ai/TOOLS.md';
|
||||
const TOOLS_CACHE_KEY = 'alphahuman.tools.cache';
|
||||
const TOOLS_CACHE_TTL = 1000 * 60 * 30; // 30 minutes
|
||||
const CACHE_VERSION = '1.0.0';
|
||||
|
||||
let cachedToolsConfig: ToolsConfig | null = null;
|
||||
|
||||
/**
|
||||
* Load TOOLS.md with caching and fallback strategy:
|
||||
* 1. Try in-memory cache
|
||||
* 2. Try localStorage cache (with TTL)
|
||||
* 3. Try GitHub remote
|
||||
* 4. Fallback to bundled TOOLS.md
|
||||
*/
|
||||
export async function loadTools(): Promise<ToolsConfig> {
|
||||
// 1. Memory cache
|
||||
if (cachedToolsConfig) {
|
||||
return cachedToolsConfig;
|
||||
}
|
||||
|
||||
// 2. Local storage cache
|
||||
try {
|
||||
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
|
||||
) {
|
||||
cachedToolsConfig = parsed.config;
|
||||
return parsed.config;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore cache errors
|
||||
}
|
||||
|
||||
let raw: string;
|
||||
let isDefault = false;
|
||||
|
||||
try {
|
||||
// 3. GitHub remote
|
||||
const response = await fetch(TOOLS_GITHUB_URL);
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
raw = await response.text();
|
||||
} catch {
|
||||
// 4. Fallback to bundled
|
||||
raw = toolsMd;
|
||||
isDefault = true;
|
||||
}
|
||||
|
||||
const config = parseTools(raw, isDefault);
|
||||
|
||||
// Cache the result
|
||||
cachedToolsConfig = config;
|
||||
try {
|
||||
const cacheEntry: ToolsCacheEntry = {
|
||||
config,
|
||||
timestamp: Date.now(),
|
||||
version: CACHE_VERSION
|
||||
};
|
||||
localStorage.setItem(TOOLS_CACHE_KEY, JSON.stringify(cacheEntry));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse TOOLS markdown into structured config
|
||||
*/
|
||||
export function parseTools(raw: string, isDefault: boolean): ToolsConfig {
|
||||
const parseResult = parseToolsFromMarkdown(raw);
|
||||
const skillGroups = groupToolsBySkill(parseResult.tools);
|
||||
const categories = generateCategories(skillGroups);
|
||||
const environments = parseEnvironments(raw);
|
||||
const statistics = generateStatistics(parseResult.tools, skillGroups, categories);
|
||||
|
||||
return {
|
||||
raw,
|
||||
tools: parseResult.tools,
|
||||
skillGroups,
|
||||
categories,
|
||||
environments,
|
||||
statistics,
|
||||
isDefault,
|
||||
loadedAt: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tools from markdown content
|
||||
*/
|
||||
function parseToolsFromMarkdown(raw: string): ToolParseResult {
|
||||
const tools: ToolDefinition[] = [];
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
try {
|
||||
// Extract tools sections
|
||||
const toolsSections = raw.split(/### .+ Tools/);
|
||||
|
||||
for (let i = 1; i < toolsSections.length; i++) {
|
||||
const section = toolsSections[i];
|
||||
const sectionTools = parseToolsSection(section, i);
|
||||
|
||||
tools.push(...sectionTools.tools);
|
||||
errors.push(...sectionTools.errors);
|
||||
warnings.push(...sectionTools.warnings);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Failed to parse tools markdown: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return { tools, errors, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tools from a single section
|
||||
*/
|
||||
function parseToolsSection(section: string, sectionIndex: number): ToolParseResult {
|
||||
const tools: ToolDefinition[] = [];
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Try to determine skillId from context
|
||||
let skillId = 'unknown';
|
||||
if (section.includes('**Category**:')) {
|
||||
// Extract from description or infer from tools
|
||||
const categoryMatch = section.match(/\*\*Category\*\*:\s*(.+)/);
|
||||
if (categoryMatch) {
|
||||
skillId = inferSkillIdFromCategory(categoryMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract individual tools
|
||||
const toolBlocks = section.split(/#### /);
|
||||
|
||||
for (let j = 1; j < toolBlocks.length; j++) {
|
||||
const toolBlock = toolBlocks[j];
|
||||
try {
|
||||
const tool = parseToolBlock(toolBlock, skillId);
|
||||
if (tool) {
|
||||
tools.push(tool);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Error parsing tool in section ${sectionIndex}, block ${j}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { tools, errors, warnings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single tool block
|
||||
*/
|
||||
function parseToolBlock(block: string, defaultSkillId: string): ToolDefinition | null {
|
||||
const lines = block.trim().split('\n');
|
||||
if (lines.length === 0) return null;
|
||||
|
||||
const name = lines[0].trim();
|
||||
if (!name) return null;
|
||||
|
||||
// Extract description
|
||||
const descriptionMatch = block.match(/\*\*Description\*\*:\s*(.+)/);
|
||||
const description = descriptionMatch?.[1]?.trim() || 'No description available';
|
||||
|
||||
// Extract parameters and build input schema
|
||||
const parametersSection = extractSection(block, 'Parameters');
|
||||
const inputSchema = parseParametersToSchema(parametersSection);
|
||||
|
||||
// Try to determine actual skillId from tool name or description
|
||||
const skillId = inferSkillIdFromTool(name, description, defaultSkillId);
|
||||
|
||||
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: []
|
||||
};
|
||||
|
||||
if (!parametersText || parametersText.includes('*None*')) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
const paramLines = parametersText.split('\n').filter(line => line.trim().startsWith('- **'));
|
||||
|
||||
for (const line of paramLines) {
|
||||
const match = line.match(/- \*\*(.+?)\*\* \((.+?)\)(\s*\*\*\(required\)\*\*)?\s*:\s*(.+)/);
|
||||
if (match) {
|
||||
const [, paramName, paramType, isRequired, paramDescription] = match;
|
||||
|
||||
schema.properties[paramName] = {
|
||||
type: paramType === 'any' ? 'string' : paramType,
|
||||
description: paramDescription.trim()
|
||||
};
|
||||
|
||||
if (isRequired) {
|
||||
schema.required?.push(paramName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group tools by skill
|
||||
*/
|
||||
function groupToolsBySkill(tools: ToolDefinition[]): Record<string, SkillGroup> {
|
||||
const groups: Record<string, SkillGroup> = {};
|
||||
|
||||
for (const tool of tools) {
|
||||
const skillId = tool.skillId;
|
||||
|
||||
if (!groups[skillId]) {
|
||||
groups[skillId] = {
|
||||
skillId,
|
||||
name: formatSkillName(skillId),
|
||||
category: categorizeSkill(skillId),
|
||||
tools: []
|
||||
};
|
||||
}
|
||||
|
||||
groups[skillId].tools.push(tool);
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate tool categories
|
||||
*/
|
||||
function generateCategories(skillGroups: Record<string, SkillGroup>): Record<string, ToolCategory> {
|
||||
const categories: Record<string, ToolCategory> = {};
|
||||
|
||||
// Initialize predefined categories
|
||||
const predefinedCategories = {
|
||||
communication: {
|
||||
id: 'communication',
|
||||
name: 'Communication',
|
||||
description: 'Tools for messaging, email, and social interaction',
|
||||
skills: ['telegram', 'gmail', 'discord', 'slack']
|
||||
},
|
||||
productivity: {
|
||||
id: 'productivity',
|
||||
name: 'Productivity',
|
||||
description: 'Tools for task management, note-taking, and organization',
|
||||
skills: ['notion', 'todoist', 'calendar', 'trello']
|
||||
},
|
||||
automation: {
|
||||
id: 'automation',
|
||||
name: 'Automation',
|
||||
description: 'Tools for workflow automation and task scheduling',
|
||||
skills: ['zapier', 'ifttt', 'scheduler', 'webhook']
|
||||
},
|
||||
data: {
|
||||
id: 'data',
|
||||
name: 'Data & Analytics',
|
||||
description: 'Tools for data processing, analysis, and storage',
|
||||
skills: ['database', 'csv', 'json', 'analytics']
|
||||
},
|
||||
media: {
|
||||
id: 'media',
|
||||
name: 'Media & Content',
|
||||
description: 'Tools for image, video, and content processing',
|
||||
skills: ['image', 'video', 'audio', 'pdf']
|
||||
},
|
||||
utility: {
|
||||
id: 'utility',
|
||||
name: 'Utilities',
|
||||
description: 'General-purpose utility tools and helpers',
|
||||
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
|
||||
);
|
||||
|
||||
categories[categoryId] = {
|
||||
...categoryConfig,
|
||||
toolCount: skillsInCategory.reduce((sum, group) => sum + group.tools.length, 0)
|
||||
};
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse environments from markdown
|
||||
*/
|
||||
function parseEnvironments(raw: string): Record<string, ToolEnvironment> {
|
||||
const environments: Record<string, ToolEnvironment> = {};
|
||||
|
||||
// Extract environment sections
|
||||
const envSection = extractSection(raw, 'Environment Configuration');
|
||||
if (!envSection) {
|
||||
// Return default environments if not found in markdown
|
||||
return getDefaultEnvironments();
|
||||
}
|
||||
|
||||
const envBlocks = envSection.split(/### (.+) Environment/);
|
||||
|
||||
for (let i = 1; i < envBlocks.length; i += 2) {
|
||||
const envName = envBlocks[i];
|
||||
const envContent = envBlocks[i + 1] || '';
|
||||
|
||||
const envId = envName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
environments[envId] = {
|
||||
id: envId,
|
||||
name: envName,
|
||||
description: extractFirstLine(envContent),
|
||||
accessLevel: extractListItem(envContent, 'Access Level'),
|
||||
rateLimits: extractListItem(envContent, 'Rate Limits'),
|
||||
authentication: extractListItem(envContent, 'Authentication'),
|
||||
logging: extractListItem(envContent, 'Logging')
|
||||
};
|
||||
}
|
||||
|
||||
return environments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate statistics
|
||||
*/
|
||||
function generateStatistics(
|
||||
tools: ToolDefinition[],
|
||||
skillGroups: Record<string, SkillGroup>,
|
||||
categories: Record<string, ToolCategory>
|
||||
): ToolStatistics {
|
||||
const toolsByCategory: Record<string, number> = {};
|
||||
const skillsByCategory: Record<string, string[]> = {};
|
||||
|
||||
for (const category of Object.keys(categories)) {
|
||||
toolsByCategory[category] = 0;
|
||||
skillsByCategory[category] = [];
|
||||
}
|
||||
|
||||
for (const group of Object.values(skillGroups)) {
|
||||
const category = group.category;
|
||||
toolsByCategory[category] += group.tools.length;
|
||||
skillsByCategory[category].push(group.skillId);
|
||||
}
|
||||
|
||||
return {
|
||||
totalTools: tools.length,
|
||||
activeSkills: Object.keys(skillGroups).length,
|
||||
categoriesCount: Object.keys(categories).length,
|
||||
toolsByCategory,
|
||||
skillsByCategory
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
function extractSection(raw: string, heading: string): string {
|
||||
const regex = new RegExp(`## ${heading}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, 'i');
|
||||
const match = raw.match(regex);
|
||||
return match?.[1]?.trim() ?? '';
|
||||
}
|
||||
|
||||
function extractFirstLine(text: string): string {
|
||||
const lines = text.split('\n').filter(line => line.trim());
|
||||
return lines[0]?.trim() || '';
|
||||
}
|
||||
|
||||
function extractListItem(text: string, itemName: string): string {
|
||||
const regex = new RegExp(`- \\*\\*${itemName}\\*\\*:\\s*(.+)`, 'i');
|
||||
const match = text.match(regex);
|
||||
return match?.[1]?.trim() || '';
|
||||
}
|
||||
|
||||
function formatSkillName(skillId: string): string {
|
||||
return skillId
|
||||
.split(/[-_]/)
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function categorizeSkill(skillId: string): string {
|
||||
const categoryMap: Record<string, string> = {
|
||||
telegram: 'communication',
|
||||
gmail: 'communication',
|
||||
discord: 'communication',
|
||||
slack: 'communication',
|
||||
notion: 'productivity',
|
||||
todoist: 'productivity',
|
||||
calendar: 'productivity',
|
||||
trello: 'productivity',
|
||||
zapier: 'automation',
|
||||
ifttt: 'automation',
|
||||
scheduler: 'automation',
|
||||
webhook: 'automation',
|
||||
database: 'data',
|
||||
csv: 'data',
|
||||
json: 'data',
|
||||
analytics: 'data',
|
||||
image: 'media',
|
||||
video: 'media',
|
||||
audio: 'media',
|
||||
pdf: 'media'
|
||||
};
|
||||
|
||||
for (const [skill, category] of Object.entries(categoryMap)) {
|
||||
if (skillId.includes(skill)) {
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
return 'utility';
|
||||
}
|
||||
|
||||
function inferSkillIdFromCategory(category: string): string {
|
||||
const categorySkillMap: Record<string, string> = {
|
||||
'Communication': 'telegram',
|
||||
'Productivity': 'notion',
|
||||
'Automation': 'zapier',
|
||||
'Data & Analytics': 'database',
|
||||
'Media & Content': 'image'
|
||||
};
|
||||
|
||||
return categorySkillMap[category] || 'unknown';
|
||||
}
|
||||
|
||||
function inferSkillIdFromTool(name: string, description: string, defaultSkillId: string): string {
|
||||
const text = `${name} ${description}`.toLowerCase();
|
||||
|
||||
const skillKeywords: Record<string, string[]> = {
|
||||
telegram: ['telegram', 'chat', 'message'],
|
||||
gmail: ['gmail', 'email', 'mail'],
|
||||
notion: ['notion', 'page', 'database'],
|
||||
discord: ['discord'],
|
||||
slack: ['slack'],
|
||||
todoist: ['todoist', 'task'],
|
||||
calendar: ['calendar', 'event'],
|
||||
trello: ['trello', 'board'],
|
||||
zapier: ['zapier'],
|
||||
ifttt: ['ifttt']
|
||||
};
|
||||
|
||||
for (const [skillId, keywords] of Object.entries(skillKeywords)) {
|
||||
if (keywords.some(keyword => text.includes(keyword))) {
|
||||
return skillId;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultSkillId;
|
||||
}
|
||||
|
||||
function getDefaultEnvironments(): Record<string, ToolEnvironment> {
|
||||
return {
|
||||
development: {
|
||||
id: 'development',
|
||||
name: 'Development',
|
||||
description: 'Local development environment with full access',
|
||||
accessLevel: 'Full access to all tools',
|
||||
rateLimits: 'Relaxed for testing',
|
||||
authentication: 'Development credentials',
|
||||
logging: 'Verbose logging enabled'
|
||||
},
|
||||
production: {
|
||||
id: 'production',
|
||||
name: 'Production',
|
||||
description: 'Production environment with security restrictions',
|
||||
accessLevel: 'Production-safe tools only',
|
||||
rateLimits: 'Standard API limits enforced',
|
||||
authentication: 'Production credentials required',
|
||||
logging: 'Essential logs only'
|
||||
},
|
||||
testing: {
|
||||
id: 'testing',
|
||||
name: 'Testing',
|
||||
description: 'Testing environment for automated validation',
|
||||
accessLevel: 'Safe tools for automated testing',
|
||||
rateLimits: 'Testing-specific limits',
|
||||
authentication: 'Test credentials',
|
||||
logging: 'Test execution logs'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear tools cache (useful for testing or manual refresh)
|
||||
*/
|
||||
export function clearToolsCache(): void {
|
||||
cachedToolsConfig = null;
|
||||
try {
|
||||
localStorage.removeItem(TOOLS_CACHE_KEY);
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Type definitions for the AI tools system.
|
||||
* Provides interfaces for tool loading, caching, and configuration management.
|
||||
*/
|
||||
|
||||
/** Tool definition from skills runtime */
|
||||
export interface ToolDefinition {
|
||||
/** Skill that provides this tool */
|
||||
skillId: string;
|
||||
/** Tool name/identifier */
|
||||
name: string;
|
||||
/** Tool description for AI understanding */
|
||||
description: string;
|
||||
/** JSON Schema for input validation */
|
||||
inputSchema: {
|
||||
type: "object";
|
||||
properties: Record<string, unknown>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/** Tool category for organization */
|
||||
export interface ToolCategory {
|
||||
/** Category identifier */
|
||||
id: string;
|
||||
/** Display name */
|
||||
name: string;
|
||||
/** Category description */
|
||||
description: string;
|
||||
/** Skills that belong to this category */
|
||||
skills: string[];
|
||||
/** Number of tools in category */
|
||||
toolCount?: number;
|
||||
}
|
||||
|
||||
/** Skill grouping with tools */
|
||||
export interface SkillGroup {
|
||||
/** Skill identifier */
|
||||
skillId: string;
|
||||
/** Display name */
|
||||
name: string;
|
||||
/** Category this skill belongs to */
|
||||
category: string;
|
||||
/** Tools provided by this skill */
|
||||
tools: ToolDefinition[];
|
||||
}
|
||||
|
||||
/** Environment configuration for tools */
|
||||
export interface ToolEnvironment {
|
||||
/** Environment identifier */
|
||||
id: string;
|
||||
/** Display name */
|
||||
name: string;
|
||||
/** Environment description */
|
||||
description: string;
|
||||
/** Access level description */
|
||||
accessLevel: string;
|
||||
/** Rate limiting information */
|
||||
rateLimits: string;
|
||||
/** Authentication requirements */
|
||||
authentication: string;
|
||||
/** Logging configuration */
|
||||
logging: string;
|
||||
}
|
||||
|
||||
/** Complete tools configuration */
|
||||
export interface ToolsConfig {
|
||||
/** Raw markdown source */
|
||||
raw: string;
|
||||
/** All discovered tools */
|
||||
tools: ToolDefinition[];
|
||||
/** Tools grouped by skill */
|
||||
skillGroups: Record<string, SkillGroup>;
|
||||
/** Tool categories */
|
||||
categories: Record<string, ToolCategory>;
|
||||
/** Available environments */
|
||||
environments: Record<string, ToolEnvironment>;
|
||||
/** Tool statistics */
|
||||
statistics: ToolStatistics;
|
||||
/** Whether this is the default config or user-customized */
|
||||
isDefault: boolean;
|
||||
/** Last loaded timestamp */
|
||||
loadedAt: number;
|
||||
}
|
||||
|
||||
/** Tool usage and availability statistics */
|
||||
export interface ToolStatistics {
|
||||
/** Total number of tools */
|
||||
totalTools: number;
|
||||
/** Number of active skills */
|
||||
activeSkills: number;
|
||||
/** Number of categories */
|
||||
categoriesCount: number;
|
||||
/** Tools by category */
|
||||
toolsByCategory: Record<string, number>;
|
||||
/** Skills by category */
|
||||
skillsByCategory: Record<string, string[]>;
|
||||
}
|
||||
|
||||
/** Tool parsing result from markdown */
|
||||
export interface ToolParseResult {
|
||||
/** Successfully parsed tools */
|
||||
tools: ToolDefinition[];
|
||||
/** Parsing errors */
|
||||
errors: string[];
|
||||
/** Warnings during parsing */
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/** Cache entry for tools configuration */
|
||||
export interface ToolsCacheEntry {
|
||||
/** Cached configuration */
|
||||
config: ToolsConfig;
|
||||
/** Cache timestamp */
|
||||
timestamp: number;
|
||||
/** Cache version for invalidation */
|
||||
version: string;
|
||||
}
|
||||
|
||||
/** Tool discovery source */
|
||||
export type ToolSource = 'runtime' | 'github' | 'bundled' | 'mock';
|
||||
|
||||
/** Tool discovery result */
|
||||
export interface ToolDiscoveryResult {
|
||||
/** Discovered tools */
|
||||
tools: ToolDefinition[];
|
||||
/** Source of the tools */
|
||||
source: ToolSource;
|
||||
/** Discovery timestamp */
|
||||
discoveredAt: number;
|
||||
/** Any errors during discovery */
|
||||
errors?: string[];
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/** Complete AI configuration combining SOUL and TOOLS */
|
||||
export interface AIConfig {
|
||||
/** SOUL persona configuration */
|
||||
soul: SoulConfig;
|
||||
/** Tools configuration */
|
||||
tools: ToolsConfig;
|
||||
/** Overall loading metadata */
|
||||
metadata: AIConfigMetadata;
|
||||
}
|
||||
|
||||
/** AI configuration loading metadata */
|
||||
export interface AIConfigMetadata {
|
||||
/** Last loaded timestamp */
|
||||
loadedAt: number;
|
||||
/** Loading duration in milliseconds */
|
||||
loadingDuration: number;
|
||||
/** Whether any component used fallback data */
|
||||
hasFallbacks: boolean;
|
||||
/** Sources used for loading */
|
||||
sources: {
|
||||
soul: 'memory' | 'localStorage' | 'github' | 'bundled';
|
||||
tools: 'memory' | 'localStorage' | 'github' | 'bundled';
|
||||
};
|
||||
/** Loading errors (non-fatal) */
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
/** AI configuration loading options */
|
||||
export interface AIConfigLoadOptions {
|
||||
/** Force reload from remote sources */
|
||||
forceRefresh?: boolean;
|
||||
/** Include detailed loading metadata */
|
||||
includeMetadata?: boolean;
|
||||
/** Timeout for remote loading (ms) */
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
/** AI configuration cache entry */
|
||||
export interface AIConfigCacheEntry {
|
||||
/** Cached configuration */
|
||||
config: AIConfig;
|
||||
/** Cache timestamp */
|
||||
timestamp: number;
|
||||
/** Cache version for invalidation */
|
||||
version: string;
|
||||
}
|
||||
|
||||
/** AI configuration loading result */
|
||||
export interface AIConfigLoadResult {
|
||||
/** Loaded configuration */
|
||||
config: AIConfig;
|
||||
/** Loading was successful */
|
||||
success: boolean;
|
||||
/** Any errors encountered */
|
||||
errors: string[];
|
||||
/** Loading duration */
|
||||
duration: number;
|
||||
}
|
||||
@@ -10,8 +10,7 @@ import Markdown from 'react-markdown';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { inferenceApi, type ModelInfo } from '../services/api/inferenceApi';
|
||||
import { loadSoul } from '../lib/ai/soul/loader';
|
||||
import { injectSoulIntoMessage } from '../lib/ai/soul/injector';
|
||||
import { injectAll } from '../lib/ai/injector';
|
||||
import type { Message } from '../lib/ai/providers/interface';
|
||||
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
||||
import {
|
||||
@@ -253,16 +252,15 @@ const Conversations = () => {
|
||||
setIsSending(true);
|
||||
|
||||
try {
|
||||
// Process user message with SOUL injection
|
||||
// Process user message with SOUL + TOOLS injection
|
||||
let processedUserContent = trimmed;
|
||||
try {
|
||||
const soulConfig = await loadSoul();
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: trimmed }]
|
||||
};
|
||||
|
||||
const injectedMessage = injectSoulIntoMessage(userMessage, soulConfig, {
|
||||
const injectedMessage = await injectAll(userMessage, {
|
||||
mode: 'context-block',
|
||||
includeMetadata: false
|
||||
});
|
||||
@@ -273,9 +271,9 @@ const Conversations = () => {
|
||||
.map(block => (block as { text: string }).text)
|
||||
.join('\n');
|
||||
|
||||
console.log('✅ SOUL injection successful in Conversations page');
|
||||
} catch (soulError) {
|
||||
console.warn('⚠️ SOUL injection failed in Conversations page:', soulError);
|
||||
console.log('✅ SOUL + TOOLS injection successful in Conversations page');
|
||||
} catch (injectionError) {
|
||||
console.warn('⚠️ SOUL + TOOLS injection failed in Conversations page:', injectionError);
|
||||
// Continue with original message
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ import type {
|
||||
ThreadsListData,
|
||||
} from '../../types/thread';
|
||||
import { apiClient } from '../apiClient';
|
||||
import { loadSoul } from '../../lib/ai/soul/loader';
|
||||
import { injectSoulIntoMessage } from '../../lib/ai/soul/injector';
|
||||
import { injectAll } from '../../lib/ai/injector';
|
||||
import type { Message } from '../../lib/ai/providers/interface';
|
||||
|
||||
export const threadApi = {
|
||||
@@ -46,7 +45,7 @@ export const threadApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/** POST /chat/sendMessage — send a user message with SOUL injection */
|
||||
/** POST /chat/sendMessage — send a user message with SOUL + TOOLS injection */
|
||||
sendMessage: async (
|
||||
message: string,
|
||||
conversationId: string,
|
||||
@@ -56,13 +55,12 @@ export const threadApi = {
|
||||
|
||||
if (options.injectSoul) {
|
||||
try {
|
||||
const soulConfig = await loadSoul();
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: message }]
|
||||
};
|
||||
|
||||
const injectedMessage = injectSoulIntoMessage(userMessage, soulConfig, {
|
||||
const injectedMessage = await injectAll(userMessage, {
|
||||
mode: 'context-block',
|
||||
includeMetadata: false
|
||||
});
|
||||
@@ -74,9 +72,10 @@ export const threadApi = {
|
||||
.join('\n');
|
||||
|
||||
processedMessage = textContent;
|
||||
console.log('✅ SOUL + TOOLS injection successful in threadApi sendMessage');
|
||||
} catch (error) {
|
||||
// Graceful degradation - log error but continue with original message
|
||||
console.warn('SOUL injection failed, using original message:', error);
|
||||
console.warn('⚠️ SOUL + TOOLS injection failed in threadApi sendMessage:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@ import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { threadApi } from '../services/api/threadApi';
|
||||
import type { Thread, ThreadMessage } from '../types/thread';
|
||||
import { loadSoul } from '../lib/ai/soul/loader';
|
||||
import { injectSoulIntoMessage } from '../lib/ai/soul/injector';
|
||||
import { injectAll } from '../lib/ai/injector';
|
||||
import type { Message } from '../lib/ai/providers/interface';
|
||||
|
||||
interface ThreadState {
|
||||
@@ -98,16 +97,15 @@ export const sendMessage = createAsyncThunk(
|
||||
try {
|
||||
dispatch(addMessageLocal({ threadId, message: userMessage }));
|
||||
|
||||
// 2. Process message with SOUL injection before sending to API
|
||||
// 2. Process message with SOUL + TOOLS injection before sending to API
|
||||
let processedMessage = message;
|
||||
try {
|
||||
const soulConfig = await loadSoul();
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: message }]
|
||||
};
|
||||
|
||||
const injectedMessage = injectSoulIntoMessage(userMessage, soulConfig, {
|
||||
const injectedMessage = await injectAll(userMessage, {
|
||||
mode: 'context-block',
|
||||
includeMetadata: false
|
||||
});
|
||||
@@ -118,9 +116,9 @@ export const sendMessage = createAsyncThunk(
|
||||
.map(block => (block as { text: string }).text)
|
||||
.join('\n');
|
||||
|
||||
console.log('✅ SOUL injection successful in Redux sendMessage thunk');
|
||||
} catch (soulError) {
|
||||
console.warn('⚠️ SOUL injection failed in Redux sendMessage thunk:', soulError);
|
||||
console.log('✅ SOUL + TOOLS injection successful in Redux sendMessage thunk');
|
||||
} catch (injectionError) {
|
||||
console.warn('⚠️ SOUL + TOOLS injection failed in Redux sendMessage thunk:', injectionError);
|
||||
// Continue with original message
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* Helper functions for invoking Tauri commands from the frontend.
|
||||
*/
|
||||
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
|
||||
import { loadSoul } from '../lib/ai/soul/loader';
|
||||
import { injectSoulIntoMessage } from '../lib/ai/soul/injector';
|
||||
import { injectAll } from '../lib/ai/injector';
|
||||
import type { Message } from '../lib/ai/providers/interface';
|
||||
|
||||
// Check if we're running in Tauri
|
||||
@@ -406,13 +405,12 @@ export async function alphahumanAgentChat(
|
||||
|
||||
if (options.injectSoul) {
|
||||
try {
|
||||
const soulConfig = await loadSoul();
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: message }]
|
||||
};
|
||||
|
||||
const injectedMessage = injectSoulIntoMessage(userMessage, soulConfig, {
|
||||
const injectedMessage = await injectAll(userMessage, {
|
||||
mode: 'context-block',
|
||||
includeMetadata: false
|
||||
});
|
||||
@@ -424,8 +422,9 @@ export async function alphahumanAgentChat(
|
||||
.join('\n');
|
||||
|
||||
processedMessage = textContent;
|
||||
console.log('✅ SOUL + TOOLS injection successful in alphahumanAgentChat');
|
||||
} catch (error) {
|
||||
console.warn('SOUL injection failed, using original message:', error);
|
||||
console.warn('⚠️ SOUL + TOOLS injection failed in alphahumanAgentChat:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user