diff --git a/apps/x/apps/main/src/composio-handler.ts b/apps/x/apps/main/src/composio-handler.ts index e5b25d1a..cb6ecde6 100644 --- a/apps/x/apps/main/src/composio-handler.ts +++ b/apps/x/apps/main/src/composio-handler.ts @@ -1,6 +1,7 @@ import { shell, BrowserWindow } from 'electron'; import { createAuthServer } from './auth-server.js'; -import * as composioClient from '@x/core/dist/composio/client.js'; +import container from '@x/core/dist/di/container.js'; +import type { IComposioService } from '@x/core/dist/execution/composio-service.js'; import { composioAccountsRepo } from '@x/core/dist/composio/repo.js'; import type { LocalConnectedAccount } from '@x/core/dist/composio/types.js'; @@ -25,11 +26,15 @@ export function emitComposioEvent(event: { toolkitSlug: string; success: boolean } } +function getComposioService(): IComposioService { + return container.resolve('composioService'); +} + /** * Check if Composio is configured with an API key */ export function isConfigured(): { configured: boolean } { - return { configured: composioClient.isConfigured() }; + return { configured: getComposioService().isConfigured() }; } /** @@ -37,7 +42,7 @@ export function isConfigured(): { configured: boolean } { */ export function setApiKey(apiKey: string): { success: boolean; error?: string } { try { - composioClient.setApiKey(apiKey); + getComposioService().setApiKey(apiKey); return { success: true }; } catch (error) { return { @@ -65,7 +70,8 @@ export async function initiateConnection(toolkitSlug: string): Promise<{ } // Get toolkit to check auth schemes - const toolkit = await composioClient.getToolkit(toolkitSlug); + const composioService = getComposioService(); + const toolkit = await composioService.getToolkit(toolkitSlug); // Check for managed OAuth2 if (!toolkit.composio_managed_auth_schemes.includes('OAUTH2')) { @@ -76,7 +82,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{ } // Find or create managed OAuth2 auth config - const authConfigs = await composioClient.listAuthConfigs(toolkitSlug, null, true); + const authConfigs = await composioService.listAuthConfigs(toolkitSlug, null, true); let authConfigId: string; const managedOauth2 = authConfigs.items.find( @@ -87,7 +93,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{ authConfigId = managedOauth2.id; } else { // Create new managed auth config - const created = await composioClient.createAuthConfig({ + const created = await composioService.createAuthConfig({ toolkit: { slug: toolkitSlug }, auth_config: { type: 'use_composio_managed_auth', @@ -99,7 +105,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{ // Create connected account with callback URL const callbackUrl = REDIRECT_URI; - const response = await composioClient.createConnectedAccount({ + const response = await composioService.createConnectedAccount({ auth_config: { id: authConfigId }, connection: { user_id: 'rowboat-user', @@ -146,7 +152,7 @@ export async function initiateConnection(toolkitSlug: string): Promise<{ const { server } = await createAuthServer(8081, async (_code, _state) => { // OAuth callback received - sync the account status try { - const accountStatus = await composioClient.getConnectedAccount(connectedAccountId); + const accountStatus = await getComposioService().getConnectedAccount(connectedAccountId); composioAccountsRepo.updateAccountStatus(toolkitSlug, accountStatus.status); if (accountStatus.status === 'ACTIVE') { @@ -228,7 +234,7 @@ export async function syncConnection( connectedAccountId: string ): Promise<{ status: string }> { try { - const accountStatus = await composioClient.getConnectedAccount(connectedAccountId); + const accountStatus = await getComposioService().getConnectedAccount(connectedAccountId); composioAccountsRepo.updateAccountStatus(toolkitSlug, accountStatus.status); return { status: accountStatus.status }; } catch (error) { @@ -245,7 +251,7 @@ export async function disconnect(toolkitSlug: string): Promise<{ success: boolea const account = composioAccountsRepo.getAccount(toolkitSlug); if (account) { // Delete from Composio - await composioClient.deleteConnectedAccount(account.id); + await getComposioService().deleteConnectedAccount(account.id); // Delete local record composioAccountsRepo.deleteAccount(toolkitSlug); } @@ -283,7 +289,7 @@ export async function executeAction( }; } - const result = await composioClient.executeAction(actionSlug, account.id, input); + const result = await getComposioService().executeAction(actionSlug, account.id, input); return result; } catch (error) { console.error('[Composio] Action execution failed:', error); diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index 2896ee7a..184e5998 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -22,8 +22,9 @@ import { RunEvent } from '@x/shared/dist/runs.js'; import { ServiceEvent } from '@x/shared/dist/service-events.js'; import container from '@x/core/dist/di/container.js'; import { listOnboardingModels } from '@x/core/dist/models/models-dev.js'; -import { testModelConnection } from '@x/core/dist/models/models.js'; import type { IModelConfigRepo } from '@x/core/dist/models/repo.js'; +import type { ILlmService } from '@x/core/dist/execution/llm-service.js'; +import type { ISttService } from '@x/core/dist/execution/stt-service.js'; import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js'; import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js'; import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js'; @@ -352,7 +353,8 @@ export function setupIpcHandlers() { return await listOnboardingModels(); }, 'models:test': async (_event, args) => { - return await testModelConnection(args.provider, args.model); + const llmService = container.resolve('llmService'); + return await llmService.testConnection(args.provider, args.model); }, 'models:saveConfig': async (_event, args) => { const repo = container.resolve('modelConfigRepo'); @@ -469,6 +471,11 @@ export function setupIpcHandlers() { const error = await shell.openPath(filePath); return { error: error || undefined }; }, + 'stt:transcribe': async (_event, args) => { + const sttService = container.resolve('sttService'); + const transcript = await sttService.transcribe(args.audioBase64, args.mimeType); + return { transcript }; + }, 'shell:readFileBase64': async (_event, args) => { let filePath = args.path; if (filePath.startsWith('~')) { diff --git a/apps/x/apps/main/src/main.ts b/apps/x/apps/main/src/main.ts index 2a5330ab..1e7c4d69 100644 --- a/apps/x/apps/main/src/main.ts +++ b/apps/x/apps/main/src/main.ts @@ -12,8 +12,9 @@ import { import { fileURLToPath, pathToFileURL } from "node:url"; import { dirname } from "node:path"; import { updateElectronApp, UpdateSourceType } from "update-electron-app"; -import { init as initGmailSync } from "@x/core/dist/knowledge/sync_gmail.js"; import { init as initCalendarSync } from "@x/core/dist/knowledge/sync_calendar.js"; +import container from "@x/core/dist/di/container.js"; +import type { IGmailService } from "@x/core/dist/execution/gmail-service.js"; import { init as initFirefliesSync } from "@x/core/dist/knowledge/sync_fireflies.js"; import { init as initGranolaSync } from "@x/core/dist/knowledge/granola/sync.js"; import { init as initGraphBuilder } from "@x/core/dist/knowledge/build_graph.js"; @@ -157,7 +158,7 @@ app.whenReady().then(async () => { startServicesWatcher(); // start gmail sync - initGmailSync(); + container.resolve('gmailService').init(); // start calendar sync initCalendarSync(); diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index 3586aaac..9218182e 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -13,8 +13,8 @@ import { import container from '@x/core/dist/di/container.js'; import { IOAuthRepo } from '@x/core/dist/auth/repo.js'; import { IClientRegistrationRepo } from '@x/core/dist/auth/client-repo.js'; -import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js'; import { triggerSync as triggerCalendarSync } from '@x/core/dist/knowledge/sync_calendar.js'; +import type { IGmailService } from '@x/core/dist/execution/gmail-service.js'; import { triggerSync as triggerFirefliesSync } from '@x/core/dist/knowledge/sync_fireflies.js'; import { emitOAuthEvent } from './ipc.js'; @@ -220,7 +220,7 @@ export async function connectProvider(provider: string, clientId?: string): Prom // Trigger immediate sync for relevant providers if (provider === 'google') { - triggerGmailSync(); + container.resolve('gmailService').triggerSync(); triggerCalendarSync(); } else if (provider === 'fireflies-ai') { triggerFirefliesSync(); diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 1bfc98c4..062340c7 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -449,30 +449,20 @@ export function SidebarContentPanel({ async function transcribeWithDeepgram(audioBlob: Blob): Promise { try { - const configResult = await window.ipc.invoke('workspace:readFile', { - path: 'config/deepgram.json', - encoding: 'utf8', + const arrayBuffer = await audioBlob.arrayBuffer() + const bytes = new Uint8Array(arrayBuffer) + let binary = '' + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]!) + } + const audioBase64 = btoa(binary) + const result = await window.ipc.invoke('stt:transcribe', { + audioBase64, + mimeType: audioBlob.type, }) - const { apiKey } = JSON.parse(configResult.data) as { apiKey: string } - if (!apiKey) throw new Error('No apiKey in deepgram.json') - - const response = await fetch( - 'https://api.deepgram.com/v1/listen?model=nova-2&smart_format=true', - { - method: 'POST', - headers: { - Authorization: `Token ${apiKey}`, - 'Content-Type': audioBlob.type, - }, - body: audioBlob, - }, - ) - - if (!response.ok) throw new Error(`Deepgram API error: ${response.status}`) - const result = await response.json() - return result.results?.channels?.[0]?.alternatives?.[0]?.transcript ?? null + return result.transcript } catch (err) { - console.error('Deepgram transcription failed:', err) + console.error('STT transcription failed:', err) return null } } diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index 09d1c721..1518b53e 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -15,8 +15,8 @@ import { CopilotAgent } from "../application/assistant/agent.js"; import { isBlocked } from "../application/lib/command-executor.js"; import container from "../di/container.js"; import { IModelConfigRepo } from "../models/repo.js"; -import { createProvider } from "../models/models.js"; import { IAgentsRepo } from "./repo.js"; +import type { ILlmService } from "../execution/llm-service.js"; import { IMonotonicallyIncreasingIdGenerator } from "../application/lib/id-gen.js"; import { IBus } from "../application/lib/bus.js"; import { IMessageQueue } from "../application/lib/message-queue.js"; @@ -41,6 +41,7 @@ export class AgentRuntime implements IAgentRuntime { private modelConfigRepo: IModelConfigRepo; private runsLock: IRunsLock; private abortRegistry: IAbortRegistry; + private llmService: ILlmService; constructor({ runsRepo, @@ -50,6 +51,7 @@ export class AgentRuntime implements IAgentRuntime { modelConfigRepo, runsLock, abortRegistry, + llmService, }: { runsRepo: IRunsRepo; idGenerator: IMonotonicallyIncreasingIdGenerator; @@ -58,6 +60,7 @@ export class AgentRuntime implements IAgentRuntime { modelConfigRepo: IModelConfigRepo; runsLock: IRunsLock; abortRegistry: IAbortRegistry; + llmService: ILlmService; }) { this.runsRepo = runsRepo; this.idGenerator = idGenerator; @@ -66,6 +69,7 @@ export class AgentRuntime implements IAgentRuntime { this.modelConfigRepo = modelConfigRepo; this.runsLock = runsLock; this.abortRegistry = abortRegistry; + this.llmService = llmService; } async trigger(runId: string): Promise { @@ -104,6 +108,7 @@ export class AgentRuntime implements IAgentRuntime { modelConfigRepo: this.modelConfigRepo, signal, abortRegistry: this.abortRegistry, + llmService: this.llmService, })) { eventCount++; if (event.type !== "llm-stream-event") { @@ -629,6 +634,7 @@ export async function* streamAgent({ modelConfigRepo, signal, abortRegistry, + llmService, }: { state: AgentState, idGenerator: IMonotonicallyIncreasingIdGenerator; @@ -637,6 +643,7 @@ export async function* streamAgent({ modelConfigRepo: IModelConfigRepo; signal: AbortSignal; abortRegistry: IAbortRegistry; + llmService: ILlmService; }): AsyncGenerator, void, unknown> { const logger = new PrefixLogger(`run-${runId}-${state.agentName}`); @@ -657,8 +664,7 @@ export async function* streamAgent({ const tools = await buildTools(agent); // set up provider + model - const provider = createProvider(modelConfig.provider); - const model = provider.languageModel(modelConfig.model); + const model = llmService.getLanguageModel(modelConfig); let loopCounter = 0; while (true) { @@ -731,6 +737,7 @@ export async function* streamAgent({ modelConfigRepo, signal, abortRegistry, + llmService, })) { yield* processEvent({ ...event, diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index feb41a7f..ecf05288 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -13,12 +13,12 @@ import * as workspace from "../../workspace/workspace.js"; import { IAgentsRepo } from "../../agents/repo.js"; import { WorkDir } from "../../config/config.js"; import { composioAccountsRepo } from "../../composio/repo.js"; -import { executeAction as executeComposioAction, isConfigured as isComposioConfigured, listToolkitTools } from "../../composio/client.js"; import { slackToolCatalog } from "../assistant/skills/slack/tool-catalog.js"; import type { ToolContext } from "./exec-tool.js"; import { generateText } from "ai"; -import { createProvider } from "../../models/models.js"; import { IModelConfigRepo } from "../../models/repo.js"; +import type { ILlmService } from "../../execution/llm-service.js"; +import type { IComposioService } from "../../execution/composio-service.js"; // Parser libraries are loaded dynamically inside parseFile.execute() // to avoid pulling pdfjs-dist's DOM polyfills into the main bundle. // Import paths are computed so esbuild cannot statically resolve them. @@ -144,8 +144,9 @@ async function executeSlackTool( return { success: false, error: 'Slack is not connected' }; } try { + const composioService = container.resolve('composioService'); const toolSlug = await resolveSlackToolSlug(hintKey); - return await executeComposioAction(toolSlug, account.id, compactObject(params)); + return await composioService.executeAction(toolSlug, account.id, compactObject(params)); } catch (error) { return { success: false, @@ -249,7 +250,8 @@ const resolveSlackToolSlug = async (hintKey: keyof typeof slackToolHints) => { const allSlug = resolveFromTools(slackToolCatalog); if (!allSlug) { - const fallback = await listToolkitTools("slack", hint.search || null); + const composioService = container.resolve('composioService'); + const fallback = await composioService.listToolkitTools("slack", hint.search || null); const fallbackSlug = resolveFromTools(fallback.items || []); if (!fallbackSlug) { throw new Error(`Unable to resolve Slack tool for ${hintKey}. Try slack-listAvailableTools.`); @@ -858,11 +860,11 @@ export const BuiltinTools: z.infer = { const base64 = buffer.toString('base64'); - // Resolve model config from DI container + // Resolve model config and LLM service from DI container const modelConfigRepo = container.resolve('modelConfigRepo'); const modelConfig = await modelConfigRepo.getConfig(); - const provider = createProvider(modelConfig.provider); - const model = provider.languageModel(modelConfig.model); + const llmService = container.resolve('llmService'); + const model = llmService.getLanguageModel(modelConfig); const userPrompt = prompt || 'Convert this file to well-structured markdown.'; @@ -1117,7 +1119,8 @@ export const BuiltinTools: z.infer = { description: 'Check if Slack is connected and ready to use. Use this before other Slack operations.', inputSchema: z.object({}), execute: async () => { - if (!isComposioConfigured()) { + const composioService = container.resolve('composioService'); + if (!composioService.isConfigured()) { return { connected: false, error: 'Composio is not configured. Please set up your Composio API key first.', @@ -1143,12 +1146,13 @@ export const BuiltinTools: z.infer = { search: z.string().optional().describe('Optional search query to filter tools (e.g., "message", "channel", "user")'), }), execute: async ({ search }: { search?: string }) => { - if (!isComposioConfigured()) { + const composioService = container.resolve('composioService'); + if (!composioService.isConfigured()) { return { success: false, error: 'Composio is not configured' }; } try { - const result = await listToolkitTools('slack', search || null); + const result = await composioService.listToolkitTools('slack', search || null); return { success: true, tools: result.items, @@ -1176,7 +1180,8 @@ export const BuiltinTools: z.infer = { } try { - const result = await executeComposioAction(toolSlug, account.id, input); + const composioService = container.resolve('composioService'); + const result = await composioService.executeAction(toolSlug, account.id, input); return result; } catch (error) { return { diff --git a/apps/x/packages/core/src/di/container.ts b/apps/x/packages/core/src/di/container.ts index d02ca7e6..2d756fb7 100644 --- a/apps/x/packages/core/src/di/container.ts +++ b/apps/x/packages/core/src/di/container.ts @@ -14,6 +14,14 @@ import { FSGranolaConfigRepo, IGranolaConfigRepo } from "../knowledge/granola/re import { IAbortRegistry, InMemoryAbortRegistry } from "../runs/abort-registry.js"; import { FSAgentScheduleRepo, IAgentScheduleRepo } from "../agent-schedule/repo.js"; import { FSAgentScheduleStateRepo, IAgentScheduleStateRepo } from "../agent-schedule/state-repo.js"; +import type { ILlmService } from "../execution/llm-service.js"; +import type { IGmailService } from "../execution/gmail-service.js"; +import type { ISttService } from "../execution/stt-service.js"; +import type { IComposioService } from "../execution/composio-service.js"; +import { LocalLlmService } from "../execution/local/local-llm-service.js"; +import { LocalGmailService } from "../execution/local/local-gmail-service.js"; +import { LocalSttService } from "../execution/local/local-stt-service.js"; +import { LocalComposioService } from "../execution/local/local-composio-service.js"; const container = createContainer({ injectionMode: InjectionMode.PROXY, @@ -37,6 +45,11 @@ container.register({ granolaConfigRepo: asClass(FSGranolaConfigRepo).singleton(), agentScheduleRepo: asClass(FSAgentScheduleRepo).singleton(), agentScheduleStateRepo: asClass(FSAgentScheduleStateRepo).singleton(), + + llmService: asClass(LocalLlmService).singleton(), + gmailService: asClass(LocalGmailService).singleton(), + sttService: asClass(LocalSttService).singleton(), + composioService: asClass(LocalComposioService).singleton(), }); export default container; \ No newline at end of file diff --git a/apps/x/packages/core/src/execution/composio-service.ts b/apps/x/packages/core/src/execution/composio-service.ts new file mode 100644 index 00000000..9968383e --- /dev/null +++ b/apps/x/packages/core/src/execution/composio-service.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import { + ZCreateAuthConfigRequest, + ZCreateAuthConfigResponse, + ZAuthConfig, + ZConnectedAccount, + ZCreateConnectedAccountRequest, + ZCreateConnectedAccountResponse, + ZDeleteOperationResponse, + ZExecuteActionResponse, + ZListResponse, + ZToolkit, +} from "../composio/types.js"; + +export interface IComposioService { + isConfigured(): boolean; + setApiKey(apiKey: string): void; + executeAction( + actionSlug: string, + connectedAccountId: string, + input: Record, + ): Promise>; + listToolkits( + cursor?: string | null, + ): Promise>>>; + getToolkit(toolkitSlug: string): Promise>; + listToolkitTools( + toolkitSlug: string, + searchQuery?: string | null, + ): Promise<{ items: Array<{ slug: string; name: string; description: string }> }>; + listAuthConfigs( + toolkitSlug: string, + cursor?: string | null, + managedOnly?: boolean, + ): Promise>>>; + createAuthConfig( + request: z.infer, + ): Promise>; + deleteAuthConfig( + authConfigId: string, + ): Promise>; + createConnectedAccount( + request: z.infer, + ): Promise>; + getConnectedAccount( + connectedAccountId: string, + ): Promise>; + deleteConnectedAccount( + connectedAccountId: string, + ): Promise>; +} diff --git a/apps/x/packages/core/src/execution/factory.ts b/apps/x/packages/core/src/execution/factory.ts new file mode 100644 index 00000000..a50a9ccd --- /dev/null +++ b/apps/x/packages/core/src/execution/factory.ts @@ -0,0 +1,30 @@ +import { ExecutionProfile } from "@x/shared/dist/execution-profile.js"; +import { ILlmService } from "./llm-service.js"; +import { IGmailService } from "./gmail-service.js"; +import { ISttService } from "./stt-service.js"; +import { IComposioService } from "./composio-service.js"; +import { LocalLlmService } from "./local/local-llm-service.js"; +import { LocalGmailService } from "./local/local-gmail-service.js"; +import { LocalSttService } from "./local/local-stt-service.js"; +import { LocalComposioService } from "./local/local-composio-service.js"; + +export interface ExecutionServices { + llm: ILlmService; + gmail: IGmailService; + stt: ISttService; + composio: IComposioService; +} + +export function createServices(profile: ExecutionProfile): ExecutionServices { + switch (profile.mode) { + case "local": + return { + llm: new LocalLlmService(), + gmail: new LocalGmailService(), + stt: new LocalSttService(), + composio: new LocalComposioService(), + }; + default: + throw new Error(`Unsupported execution profile mode: ${(profile as { mode: string }).mode}`); + } +} diff --git a/apps/x/packages/core/src/execution/gmail-service.ts b/apps/x/packages/core/src/execution/gmail-service.ts new file mode 100644 index 00000000..6e557450 --- /dev/null +++ b/apps/x/packages/core/src/execution/gmail-service.ts @@ -0,0 +1,4 @@ +export interface IGmailService { + init(): void; + triggerSync(): void; +} diff --git a/apps/x/packages/core/src/execution/index.ts b/apps/x/packages/core/src/execution/index.ts new file mode 100644 index 00000000..cb7d6f8d --- /dev/null +++ b/apps/x/packages/core/src/execution/index.ts @@ -0,0 +1,12 @@ +export type { ILlmService } from "./llm-service.js"; +export type { IGmailService } from "./gmail-service.js"; +export type { ISttService } from "./stt-service.js"; +export type { IComposioService } from "./composio-service.js"; +export { createServices } from "./factory.js"; +export type { ExecutionServices } from "./factory.js"; + +// Local implementations +export { LocalLlmService } from "./local/local-llm-service.js"; +export { LocalGmailService } from "./local/local-gmail-service.js"; +export { LocalSttService } from "./local/local-stt-service.js"; +export { LocalComposioService } from "./local/local-composio-service.js"; diff --git a/apps/x/packages/core/src/execution/llm-service.ts b/apps/x/packages/core/src/execution/llm-service.ts new file mode 100644 index 00000000..29a682a1 --- /dev/null +++ b/apps/x/packages/core/src/execution/llm-service.ts @@ -0,0 +1,12 @@ +import { LanguageModel } from "ai"; +import { z } from "zod"; +import { LlmModelConfig, LlmProvider } from "@x/shared/dist/models.js"; + +export interface ILlmService { + getLanguageModel(config: z.infer): LanguageModel; + testConnection( + provider: z.infer, + model: string, + timeoutMs?: number, + ): Promise<{ success: boolean; error?: string }>; +} diff --git a/apps/x/packages/core/src/execution/local/local-composio-service.ts b/apps/x/packages/core/src/execution/local/local-composio-service.ts new file mode 100644 index 00000000..9f8224f0 --- /dev/null +++ b/apps/x/packages/core/src/execution/local/local-composio-service.ts @@ -0,0 +1,88 @@ +import { z } from "zod"; +import { IComposioService } from "../composio-service.js"; +import * as composioClient from "../../composio/client.js"; +import { + ZCreateAuthConfigRequest, + ZCreateAuthConfigResponse, + ZAuthConfig, + ZConnectedAccount, + ZCreateConnectedAccountRequest, + ZCreateConnectedAccountResponse, + ZDeleteOperationResponse, + ZExecuteActionResponse, + ZListResponse, + ZToolkit, +} from "../../composio/types.js"; + +export class LocalComposioService implements IComposioService { + isConfigured(): boolean { + return composioClient.isConfigured(); + } + + setApiKey(apiKey: string): void { + composioClient.setApiKey(apiKey); + } + + async executeAction( + actionSlug: string, + connectedAccountId: string, + input: Record, + ): Promise> { + return composioClient.executeAction(actionSlug, connectedAccountId, input); + } + + async listToolkits( + cursor: string | null = null, + ): Promise>>> { + return composioClient.listToolkits(cursor); + } + + async getToolkit(toolkitSlug: string): Promise> { + return composioClient.getToolkit(toolkitSlug); + } + + async listToolkitTools( + toolkitSlug: string, + searchQuery: string | null = null, + ): Promise<{ items: Array<{ slug: string; name: string; description: string }> }> { + return composioClient.listToolkitTools(toolkitSlug, searchQuery); + } + + async listAuthConfigs( + toolkitSlug: string, + cursor: string | null = null, + managedOnly: boolean = false, + ): Promise>>> { + return composioClient.listAuthConfigs(toolkitSlug, cursor, managedOnly); + } + + async createAuthConfig( + request: z.infer, + ): Promise> { + return composioClient.createAuthConfig(request); + } + + async deleteAuthConfig( + authConfigId: string, + ): Promise> { + return composioClient.deleteAuthConfig(authConfigId); + } + + async createConnectedAccount( + request: z.infer, + ): Promise> { + return composioClient.createConnectedAccount(request); + } + + async getConnectedAccount( + connectedAccountId: string, + ): Promise> { + return composioClient.getConnectedAccount(connectedAccountId); + } + + async deleteConnectedAccount( + connectedAccountId: string, + ): Promise> { + return composioClient.deleteConnectedAccount(connectedAccountId); + } +} diff --git a/apps/x/packages/core/src/execution/local/local-gmail-service.ts b/apps/x/packages/core/src/execution/local/local-gmail-service.ts new file mode 100644 index 00000000..86e9145e --- /dev/null +++ b/apps/x/packages/core/src/execution/local/local-gmail-service.ts @@ -0,0 +1,12 @@ +import { IGmailService } from "../gmail-service.js"; +import { init, triggerSync } from "../../knowledge/sync_gmail.js"; + +export class LocalGmailService implements IGmailService { + init(): void { + init(); + } + + triggerSync(): void { + triggerSync(); + } +} diff --git a/apps/x/packages/core/src/execution/local/local-llm-service.ts b/apps/x/packages/core/src/execution/local/local-llm-service.ts new file mode 100644 index 00000000..c3ac5472 --- /dev/null +++ b/apps/x/packages/core/src/execution/local/local-llm-service.ts @@ -0,0 +1,20 @@ +import { LanguageModel } from "ai"; +import { z } from "zod"; +import { LlmModelConfig, LlmProvider } from "@x/shared/dist/models.js"; +import { ILlmService } from "../llm-service.js"; +import { createProvider, testModelConnection } from "../../models/models.js"; + +export class LocalLlmService implements ILlmService { + getLanguageModel(config: z.infer): LanguageModel { + const provider = createProvider(config.provider); + return provider.languageModel(config.model); + } + + async testConnection( + providerConfig: z.infer, + model: string, + timeoutMs?: number, + ): Promise<{ success: boolean; error?: string }> { + return testModelConnection(providerConfig, model, timeoutMs); + } +} diff --git a/apps/x/packages/core/src/execution/local/local-stt-service.ts b/apps/x/packages/core/src/execution/local/local-stt-service.ts new file mode 100644 index 00000000..4dff9e0f --- /dev/null +++ b/apps/x/packages/core/src/execution/local/local-stt-service.ts @@ -0,0 +1,46 @@ +import fs from "fs"; +import path from "path"; +import { ISttService } from "../stt-service.js"; +import { WorkDir } from "../../config/config.js"; + +export class LocalSttService implements ISttService { + async transcribe(audioBase64: string, mimeType: string): Promise { + try { + const configPath = path.join(WorkDir, 'config', 'deepgram.json'); + if (!fs.existsSync(configPath)) { + throw new Error('Deepgram config not found'); + } + + const configData = fs.readFileSync(configPath, 'utf-8'); + const { apiKey } = JSON.parse(configData) as { apiKey: string }; + if (!apiKey) throw new Error('No apiKey in deepgram.json'); + + const audioBuffer = Buffer.from(audioBase64, 'base64'); + + const response = await fetch( + 'https://api.deepgram.com/v1/listen?model=nova-2&smart_format=true', + { + method: 'POST', + headers: { + Authorization: `Token ${apiKey}`, + 'Content-Type': mimeType, + }, + body: audioBuffer, + }, + ); + + if (!response.ok) throw new Error(`Deepgram API error: ${response.status}`); + const result = await response.json() as { + results?: { + channels?: Array<{ + alternatives?: Array<{ transcript?: string }>; + }>; + }; + }; + return result.results?.channels?.[0]?.alternatives?.[0]?.transcript ?? null; + } catch (err) { + console.error('Deepgram transcription failed:', err); + return null; + } + } +} diff --git a/apps/x/packages/core/src/execution/stt-service.ts b/apps/x/packages/core/src/execution/stt-service.ts new file mode 100644 index 00000000..373ac562 --- /dev/null +++ b/apps/x/packages/core/src/execution/stt-service.ts @@ -0,0 +1,3 @@ +export interface ISttService { + transcribe(audioBase64: string, mimeType: string): Promise; +} diff --git a/apps/x/packages/shared/src/execution-profile.ts b/apps/x/packages/shared/src/execution-profile.ts new file mode 100644 index 00000000..7dd795b3 --- /dev/null +++ b/apps/x/packages/shared/src/execution-profile.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const ExecutionProfile = z.discriminatedUnion('mode', [ + z.object({ mode: z.literal('local') }), + z.object({ + mode: z.literal('cloud'), + session: z.object({ + accessToken: z.string(), + refreshToken: z.string().optional(), + userId: z.string(), + }), + }), +]); + +export type ExecutionProfile = z.infer; diff --git a/apps/x/packages/shared/src/index.ts b/apps/x/packages/shared/src/index.ts index 4f10fc82..f84804c1 100644 --- a/apps/x/packages/shared/src/index.ts +++ b/apps/x/packages/shared/src/index.ts @@ -7,4 +7,5 @@ export * as mcp from './mcp.js'; export * as agentSchedule from './agent-schedule.js'; export * as agentScheduleState from './agent-schedule-state.js'; export * as serviceEvents from './service-events.js'; +export * as executionProfile from './execution-profile.js'; export { PrefixLogger }; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 175c409e..c4db165a 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -395,6 +395,10 @@ const ipcSchemas = { req: z.object({ path: z.string() }), res: z.object({ data: z.string(), mimeType: z.string(), size: z.number() }), }, + 'stt:transcribe': { + req: z.object({ audioBase64: z.string(), mimeType: z.string() }), + res: z.object({ transcript: z.string().nullable() }), + }, } as const; // ============================================================================