Enhance development scripts and improve tool documentation

- Updated `package.json` scripts for better error handling and streamlined execution of Tauri commands.
- Added a new `gmail-skill-integration.md` document detailing the Gmail skill integration process, including OAuth2 flow and tool definitions.
- Improved formatting and consistency in `TOOLS.md` for better readability and clarity.
- Enhanced logging in skill management and transport layers for better debugging and monitoring of tool calls.
This commit is contained in:
M3gA-Mind
2026-03-09 19:27:48 +05:30
parent 675335d1fe
commit f91e409a0d
16 changed files with 1234 additions and 661 deletions
+8 -6
View File
@@ -1,8 +1,9 @@
import { useState } from 'react';
import type { OAuthProviderConfig } from '../../types/oauth';
import { IS_DEV } from '../../utils/config';
import { openUrl } from '../../utils/openUrl';
import { isTauri } from '../../utils/tauriCommands';
import { IS_DEV } from '../../utils/config';
interface OAuthProviderButtonProps {
provider: OAuthProviderConfig;
@@ -25,7 +26,9 @@ const OAuthProviderButton = ({
if (IS_DEV) {
console.log(`[dev] OAuth debug mode enabled. OAuth URL: ${provider.loginUrl}`);
console.log('[dev] In debug mode, OAuth will return JSON response instead of redirect.');
console.log('[dev] After OAuth completion, copy the loginToken and use: window.__simulateDeepLink("alphahuman://auth?token=YOUR_TOKEN")');
console.log(
'[dev] After OAuth completion, copy the loginToken and use: window.__simulateDeepLink("alphahuman://auth?token=YOUR_TOKEN")'
);
}
setIsLoading(true);
@@ -51,18 +54,17 @@ const OAuthProviderButton = ({
<button
onClick={handleOAuthLogin}
disabled={isDisabled}
className={`w-full flex items-center justify-center space-x-3 ${provider.color} ${provider.hoverColor} ${provider.textColor} font-semibold py-4 rounded-xl transition-all duration-300 hover:shadow-medium hover:scale-[1.02] active:scale-[0.98] disabled:hover:scale-100 disabled:opacity-50 disabled:cursor-not-allowed ${className}`}
>
className={`w-full flex items-center justify-center space-x-3 ${provider.color} ${provider.hoverColor} font-semibold py-4 rounded-xl transition-all duration-300 hover:shadow-medium hover:scale-[1.02] active:scale-[0.98] disabled:hover:scale-100 disabled:opacity-50 disabled:cursor-not-allowed ${className}`}>
{isLoading ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-current"></div>
) : (
<IconComponent className="w-5 h-5" />
)}
<span>
<span className={provider.textColor}>
{isLoading ? 'Connecting...' : `Continue with ${provider.name}`}
</span>
</button>
);
};
export default OAuthProviderButton;
export default OAuthProviderButton;
+20 -8
View File
@@ -7,28 +7,40 @@ import { BACKEND_URL, IS_DEV } from '../../utils/config';
// Provider Icons
const GoogleIcon = ({ className = '' }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
);
const TwitterIcon = ({ className = '' }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
const GitHubIcon = ({ className = '' }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
);
const DiscordIcon = ({ className = '' }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419-.0189 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1568 2.4189Z"/>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419-.0189 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1568 2.4189Z" />
</svg>
);
@@ -73,4 +85,4 @@ export const oauthProviderConfigs: OAuthProviderConfig[] = [
export const getProviderConfig = (provider: string): OAuthProviderConfig | undefined => {
return oauthProviderConfigs.find(config => config.id === provider);
};
};
+5 -1
View File
@@ -226,11 +226,15 @@ class SkillManager {
name: string,
args: Record<string, unknown>,
): Promise<{ content: Array<{ type: string; text: string }>; isError: boolean }> {
console.log(`[SkillManager] callTool skill="${skillId}" tool="${name}"`);
const runtime = this.runtimes.get(skillId);
if (!runtime) {
console.error(`[SkillManager] callTool failed — skill "${skillId}" has no running runtime`);
throw new Error(`Skill ${skillId} is not running`);
}
return runtime.callTool(name, args);
const result = await runtime.callTool(name, args);
console.log(`[SkillManager] callTool result skill="${skillId}" tool="${name}" isError=${result.isError}`);
return result;
}
/**
+4 -1
View File
@@ -141,7 +141,10 @@ export class SkillRuntime {
content: Array<{ type: string; text: string }>;
isError: boolean;
}> {
return this.transport.request("tools/call", { name, arguments: args });
console.log(`[SkillRuntime] callTool skill="${this.manifest.id}" tool="${name}"`);
const result = await this.transport.request<{ content: Array<{ type: string; text: string }>; isError: boolean }>("tools/call", { name, arguments: args });
console.log(`[SkillRuntime] tools/call response skill="${this.manifest.id}" tool="${name}" isError=${result.isError}`);
return result;
}
/**
+2 -9
View File
@@ -51,11 +51,7 @@ export class SkillTransport {
throw new Error("Skill transport not started");
}
console.log("[skill-transport] Sending request", {
skillId: this.skillId,
method,
hasParams: params !== undefined,
});
console.log("[skill-transport] →", { skillId: this.skillId, method, params });
const result = await invoke<T>("runtime_rpc", {
skillId: this.skillId,
@@ -63,10 +59,7 @@ export class SkillTransport {
params: params ?? {},
});
console.debug("[skill-transport] Received response", {
skillId: this.skillId,
method,
});
console.log("[skill-transport] ←", { skillId: this.skillId, method, result });
return result;
}
+110 -13
View File
@@ -9,9 +9,15 @@ import {
import Markdown from 'react-markdown';
import { useNavigate, useParams } from 'react-router-dom';
import { inferenceApi, type ModelInfo } from '../services/api/inferenceApi';
import { injectAll } from '../lib/ai/injector';
import type { Message } from '../lib/ai/providers/interface';
import { skillManager } from '../lib/skills/manager';
import {
type ChatMessage,
inferenceApi,
type ModelInfo,
type Tool,
} from '../services/api/inferenceApi';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
addInferenceResponse,
@@ -63,6 +69,8 @@ const Conversations = () => {
isLoadingSuggestions,
} = useAppSelector(state => state.thread);
const skillsState = useAppSelector(state => state.skills);
const [showPurgeConfirm, setShowPurgeConfirm] = useState(false);
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
const [inputValue, setInputValue] = useState('');
@@ -255,14 +263,11 @@ const Conversations = () => {
// Process user message with SOUL + TOOLS injection
let processedUserContent = trimmed;
try {
const userMessage: Message = {
role: 'user',
content: [{ type: 'text', text: trimmed }]
};
const userMessage: Message = { role: 'user', content: [{ type: 'text', text: trimmed }] };
const injectedMessage = await injectAll(userMessage, {
mode: 'context-block',
includeMetadata: false
includeMetadata: false,
});
// Extract the processed text
@@ -277,7 +282,7 @@ const Conversations = () => {
// Continue with original message
}
const chatMessages = [
const chatMessages: ChatMessage[] = [
...historySnapshot.map(m => ({
role: (m.sender === 'user' ? 'user' : 'assistant') as 'user' | 'assistant',
content: m.content,
@@ -285,13 +290,105 @@ const Conversations = () => {
{ role: 'user' as const, content: processedUserContent },
];
const response = await inferenceApi.createChatCompletion({
model: selectedModel,
messages: chatMessages,
});
// Build tool definitions for ALL ready skills — namespaced as {skillId}__{toolName}
const allSkillTools: Tool[] = Object.entries(skillsState.skills)
.filter(([, skill]) => skill.status === 'ready' && skill.tools?.length)
.flatMap(([skillId, skill]) =>
(skill.tools ?? []).map(t => ({
type: 'function' as const,
function: {
name: `${skillId}__${t.name}`,
description: t.description,
parameters: t.inputSchema as Tool['function']['parameters'],
},
}))
);
const content = response.choices[0]?.message?.content ?? '';
dispatch(addInferenceResponse({ content }));
console.log(
`[Conversations] active skill tools: ${allSkillTools.length}`,
allSkillTools.map(t => t.function.name)
);
// Agentic tool calling loop — handles multi-turn tool execution
const loopMessages = [...chatMessages];
let finalContent = '';
const MAX_TOOL_ROUNDS = 5;
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
const response = await inferenceApi.createChatCompletion({
model: selectedModel,
messages: loopMessages,
...(allSkillTools.length > 0 ? { tools: allSkillTools, tool_choice: 'auto' } : {}),
});
console.log('🚀 ~ handleSendMessage ~ response:', response);
const choice = response.choices[0];
if (!choice) break;
const { finish_reason, message } = choice;
if (finish_reason === 'tool_calls' && message.tool_calls?.length) {
// Append assistant message with tool_calls
loopMessages.push({
role: 'assistant',
content: message.content ?? '',
tool_calls: message.tool_calls,
});
// Execute each tool and collect results
for (const tc of message.tool_calls) {
const dunderIdx = tc.function.name.indexOf('__');
const skillId = dunderIdx !== -1 ? tc.function.name.substring(0, dunderIdx) : '';
const toolName =
dunderIdx !== -1 ? tc.function.name.substring(dunderIdx + 2) : tc.function.name;
console.log(
`[Conversations] tool_call dispatched — skill="${skillId}" tool="${toolName}" call_id="${tc.id}"`
);
let toolResultContent = '';
try {
let toolArgs: Record<string, unknown> = {};
try {
toolArgs = JSON.parse(tc.function.arguments) as Record<string, unknown>;
} catch {
toolArgs = {};
}
console.log(
`[Conversations] calling skillManager.callTool("${skillId}", "${toolName}")`,
toolArgs
);
const result = await skillManager.callTool(skillId, toolName, toolArgs);
toolResultContent = result.content.map(c => c.text).join('\n');
if (result.isError) {
console.warn(
`[Conversations] tool "${toolName}" returned an error:`,
toolResultContent
);
toolResultContent = `Error: ${toolResultContent}`;
} else {
console.log(
`[Conversations] tool "${toolName}" succeeded:`,
toolResultContent.slice(0, 200)
);
}
} catch (toolErr) {
console.error(`[Conversations] tool "${toolName}" threw:`, toolErr);
toolResultContent = `Tool execution failed: ${toolErr instanceof Error ? toolErr.message : String(toolErr)}`;
}
loopMessages.push({ role: 'tool', tool_call_id: tc.id, content: toolResultContent });
}
// Continue loop to get final response after tool results
continue;
}
// Normal (non-tool) response — done
finalContent = message.content ?? '';
break;
}
dispatch(addInferenceResponse({ content: finalContent }));
} catch (err) {
dispatch(removeOptimisticMessages());
const msg =
+34 -2
View File
@@ -2,11 +2,41 @@ import { apiClient } from '../apiClient';
// ── Request types ────────────────────────────────────────────────────────────
export type ChatRole = 'system' | 'user' | 'assistant';
export type ChatRole = 'system' | 'user' | 'assistant' | 'tool';
export interface ChatMessage {
role: ChatRole;
content: string;
/** tool_call_id for role=tool messages */
tool_call_id?: string;
/** tool_calls emitted by the assistant */
tool_calls?: ToolCall[];
}
// ── Tool calling types (OpenAI-compatible) ───────────────────────────────────
export interface ToolFunction {
name: string;
description: string;
parameters: {
type: 'object';
properties: Record<string, unknown>;
required?: string[];
};
}
export interface Tool {
type: 'function';
function: ToolFunction;
}
export interface ToolCall {
id: string;
type: 'function';
function: {
name: string;
arguments: string;
};
}
export interface ChatCompletionRequest {
@@ -15,6 +45,8 @@ export interface ChatCompletionRequest {
stream?: boolean;
temperature?: number;
max_tokens?: number;
tools?: Tool[];
tool_choice?: 'none' | 'auto' | 'required';
}
export interface TextCompletionRequest {
@@ -41,7 +73,7 @@ export interface ModelsListResponse {
export interface ChatCompletionChoice {
index: number;
message: ChatMessage;
message: ChatMessage & { tool_calls?: ToolCall[] };
finish_reason: string | null;
}
+8 -13
View File
@@ -27,8 +27,8 @@ export class DaemonHealthService {
async setupHealthListener(): Promise<UnlistenFn | null> {
console.log('[DaemonHealth] setupHealthListener() called - starting setup process');
try {
console.log('[DaemonHealth] About to call listen() for alphahuman:health event');
console.log('[DaemonHealth] Setting up alphahuman:health event listener');
// console.log('[DaemonHealth] About to call listen() for alphahuman:health event');
// console.log('[DaemonHealth] Setting up alphahuman:health event listener');
this.healthEventListener = await listen<unknown>('alphahuman:health', event => {
console.log('[DaemonHealth] Received health event:', event.payload);
@@ -44,11 +44,11 @@ export class DaemonHealthService {
console.log('[DaemonHealth] alphahuman:health listener created successfully');
// Start initial timeout
console.log('[DaemonHealth] Starting health timeout');
// console.log('[DaemonHealth] Starting health timeout');
this.startHealthTimeout();
console.log('[DaemonHealth] Health timeout started');
// console.log('[DaemonHealth] Health timeout started');
console.log('[DaemonHealth] Health listener setup complete');
// console.log('[DaemonHealth] Health listener setup complete');
return this.healthEventListener;
} catch (error) {
console.error('[DaemonHealth] Failed to setup health listener:', error);
@@ -147,7 +147,7 @@ export class DaemonHealthService {
// Update the health snapshot in Redux
store.dispatch(updateHealthSnapshot({ userId, healthSnapshot: snapshot }));
console.log('[DaemonHealth] Updated health snapshot for user:', userId, snapshot);
// console.log('[DaemonHealth] Updated health snapshot for user:', userId, snapshot);
} catch (error) {
console.error('[DaemonHealth] Error updating Redux from health:', error);
}
@@ -175,12 +175,7 @@ export class DaemonHealthService {
}, this.HEALTH_TIMEOUT_MS);
// Store timeout ID in Redux for cleanup
store.dispatch(
setHealthTimeoutId({
userId,
timeoutId: this.healthTimeoutId.toString(),
})
);
store.dispatch(setHealthTimeoutId({ userId, timeoutId: this.healthTimeoutId.toString() }));
}
/**
@@ -204,4 +199,4 @@ export class DaemonHealthService {
}
// Export singleton instance
export const daemonHealthService = new DaemonHealthService();
export const daemonHealthService = new DaemonHealthService();