feat: migrate tools and skills to the new architecture; support profile-owned skills
This commit is contained in:
@@ -6,6 +6,8 @@ __pycache__/
|
||||
**/build/
|
||||
**/node_modules/
|
||||
**/.venv/
|
||||
**/.last-skill-deps-sync
|
||||
**/.last-source-deps-sync
|
||||
test/coverage/
|
||||
**/tmp/*
|
||||
core/config/**/*.json
|
||||
|
||||
@@ -6,14 +6,8 @@ import type { IntentObject, NLPAction } from '@sdk/types'
|
||||
import {
|
||||
CODEBASE_PATH,
|
||||
LEON_HOME_PATH,
|
||||
LEON_PROFILE_PATH,
|
||||
LEON_TOOLKITS_PATH,
|
||||
PROFILE_CONTEXT_PATH,
|
||||
PROFILE_MEMORY_DB_PATH,
|
||||
PROFILE_MEMORY_PATH,
|
||||
PROFILE_SKILLS_PATH,
|
||||
PROFILE_TOOLS_PATH
|
||||
} from '@@/server/src/constants'
|
||||
LEON_PROFILE_PATH
|
||||
} from '@@/server/src/leon-roots'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const runtimeIndex = args.indexOf('--runtime')
|
||||
@@ -36,15 +30,19 @@ export const RUNTIME = runtime
|
||||
export {
|
||||
CODEBASE_PATH,
|
||||
LEON_HOME_PATH,
|
||||
LEON_PROFILE_PATH,
|
||||
LEON_TOOLKITS_PATH,
|
||||
PROFILE_CONTEXT_PATH,
|
||||
PROFILE_MEMORY_DB_PATH,
|
||||
PROFILE_MEMORY_PATH,
|
||||
PROFILE_SKILLS_PATH,
|
||||
PROFILE_TOOLS_PATH
|
||||
LEON_PROFILE_PATH
|
||||
}
|
||||
|
||||
export const LEON_TOOLKITS_PATH = path.join(LEON_HOME_PATH, 'toolkits')
|
||||
export const PROFILE_CONTEXT_PATH = path.join(LEON_PROFILE_PATH, 'context')
|
||||
export const PROFILE_MEMORY_PATH = path.join(LEON_PROFILE_PATH, 'memory')
|
||||
export const PROFILE_MEMORY_DB_PATH = path.join(
|
||||
PROFILE_MEMORY_PATH,
|
||||
'index.sqlite'
|
||||
)
|
||||
export const PROFILE_SKILLS_PATH = path.join(LEON_PROFILE_PATH, 'skills')
|
||||
export const PROFILE_TOOLS_PATH = path.join(LEON_PROFILE_PATH, 'tools')
|
||||
|
||||
const BIN_PATH = path.join(LEON_HOME_PATH, 'bin')
|
||||
const BRIDGES_PATH = path.join(CODEBASE_PATH, 'bridges')
|
||||
const NODEJS_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'nodejs')
|
||||
@@ -54,11 +52,12 @@ const NODEJS_BRIDGE_VERSION_FILE_PATH = path.join(
|
||||
'version.ts'
|
||||
)
|
||||
|
||||
export const TOOLKITS_PATH = path.join(BRIDGES_PATH, 'toolkits')
|
||||
export const TOOLS_PATH = path.join(CODEBASE_PATH, 'tools')
|
||||
export const PROFILE_DISABLED_PATH = path.join(LEON_PROFILE_PATH, 'disabled.json')
|
||||
|
||||
export const [, NODEJS_BRIDGE_VERSION] = fs
|
||||
.readFileSync(NODEJS_BRIDGE_VERSION_FILE_PATH, 'utf8')
|
||||
.split("'")
|
||||
.split('\'')
|
||||
|
||||
let parsedIntentObject: IntentObject | null = null
|
||||
if (INTENT_OBJ_FILE_PATH) {
|
||||
@@ -82,11 +81,11 @@ export const PYTORCH_TORCH_PATH = path.join(PYTORCH_PATH, 'torch')
|
||||
export const SKILLS_PATH = path.join(CODEBASE_PATH, 'skills')
|
||||
export const SKILL_PATH =
|
||||
runtime === 'skill' && parsedIntentObject
|
||||
? path.join(SKILLS_PATH, parsedIntentObject.skill_name)
|
||||
? path.dirname(parsedIntentObject.skill_config_path)
|
||||
: ''
|
||||
const SKILL_LOCALE_CONFIG_CONTENT =
|
||||
runtime === 'skill' && INTENT_OBJ_FILE_PATH && parsedIntentObject
|
||||
? (() => {
|
||||
? ((): SkillLocaleConfigSchema => {
|
||||
const skillLocalePath = path.join(
|
||||
SKILL_PATH,
|
||||
'locales',
|
||||
@@ -96,17 +95,16 @@ const SKILL_LOCALE_CONFIG_CONTENT =
|
||||
fs.existsSync(skillLocalePath)
|
||||
? fs.readFileSync(skillLocalePath, 'utf8')
|
||||
: `{"variables": {}, "common_answers": {}, "widget_contents": {}, "actions": {"${parsedIntentObject.action_name}": {}}}`
|
||||
)
|
||||
) as SkillLocaleConfigSchema
|
||||
})()
|
||||
: {
|
||||
variables: {},
|
||||
common_answers: {},
|
||||
widget_contents: {},
|
||||
actions: {}
|
||||
}
|
||||
} satisfies SkillLocaleConfigSchema
|
||||
|
||||
export const SKILL_LOCALE_CONFIG: SkillLocaleConfigSchema &
|
||||
SkillLocaleConfigSchema['actions'][NLPAction] = {
|
||||
export const SKILL_LOCALE_CONFIG = {
|
||||
variables: SKILL_LOCALE_CONFIG_CONTENT.variables,
|
||||
common_answers: SKILL_LOCALE_CONFIG_CONTENT.common_answers,
|
||||
widget_contents: SKILL_LOCALE_CONFIG_CONTENT.widget_contents,
|
||||
@@ -115,4 +113,4 @@ export const SKILL_LOCALE_CONFIG: SkillLocaleConfigSchema &
|
||||
parsedIntentObject.action_name as NLPAction
|
||||
]
|
||||
: {}) || {})
|
||||
}
|
||||
} as SkillLocaleConfigSchema & SkillLocaleConfigSchema['actions'][NLPAction]
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import path from 'node:path'
|
||||
import url from 'node:url'
|
||||
import { createRequire, registerHooks } from 'node:module'
|
||||
|
||||
import { FileHelper } from '@/helpers/file-helper'
|
||||
|
||||
import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import {
|
||||
INTENT_OBJECT,
|
||||
PROFILE_SKILLS_PATH,
|
||||
SKILLS_PATH
|
||||
} from '@bridge/constants'
|
||||
import { INTENT_OBJECT, SKILL_PATH } from '@bridge/constants'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { setToolReporter } from '@sdk/tool-reporter'
|
||||
@@ -41,58 +35,6 @@ const resolveActionFunction = (actionModule: unknown): ActionFunction | null =>
|
||||
return null
|
||||
}
|
||||
|
||||
const isBarePackageImport = (specifier: string): boolean => {
|
||||
return !specifier.startsWith('.') &&
|
||||
!specifier.startsWith('/') &&
|
||||
!specifier.startsWith('node:') &&
|
||||
!specifier.startsWith('file:')
|
||||
}
|
||||
|
||||
const isLeonAliasImport = (specifier: string): boolean => {
|
||||
return specifier.startsWith('@/') ||
|
||||
specifier.startsWith('@bridge/') ||
|
||||
specifier.startsWith('@sdk/') ||
|
||||
specifier.startsWith('@@/')
|
||||
}
|
||||
|
||||
const registerSkillRuntimeNodeModules = (skillName: string): void => {
|
||||
const runtimeNodeModulesPath = path.join(
|
||||
PROFILE_SKILLS_PATH,
|
||||
skillName,
|
||||
'.runtime',
|
||||
'node_modules'
|
||||
)
|
||||
|
||||
if (!FileHelper.isExistingPath(runtimeNodeModulesPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const runtimeRequire = createRequire(
|
||||
path.join(runtimeNodeModulesPath, '__resolver__.cjs')
|
||||
)
|
||||
|
||||
// Keep Leon aliases and relative imports on the default path, and only
|
||||
// redirect bare package imports to the skill-local runtime dependencies.
|
||||
registerHooks({
|
||||
resolve(specifier, context, nextResolve) {
|
||||
if (!isBarePackageImport(specifier) || isLeonAliasImport(specifier)) {
|
||||
return nextResolve(specifier, context)
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedPath = runtimeRequire.resolve(specifier)
|
||||
|
||||
return {
|
||||
shortCircuit: true,
|
||||
url: url.pathToFileURL(resolvedPath).href
|
||||
}
|
||||
} catch {
|
||||
return nextResolve(specifier, context)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
setToolReporter(async (input) => {
|
||||
await leon.answer(input)
|
||||
@@ -108,8 +50,6 @@ async function main(): Promise<void> {
|
||||
extra_context
|
||||
} = INTENT_OBJECT
|
||||
|
||||
registerSkillRuntimeNodeModules(skill_name)
|
||||
|
||||
const params: ActionParams = {
|
||||
lang,
|
||||
utterance: INTENT_OBJECT.utterance as ActionParams['utterance'],
|
||||
@@ -129,8 +69,7 @@ async function main(): Promise<void> {
|
||||
try {
|
||||
const actionModule = await FileHelper.dynamicImportFromFile(
|
||||
path.join(
|
||||
SKILLS_PATH,
|
||||
skill_name,
|
||||
SKILL_PATH,
|
||||
'src',
|
||||
'actions',
|
||||
`${action_name}.ts`
|
||||
|
||||
@@ -129,7 +129,13 @@ export abstract class Tool {
|
||||
*/
|
||||
protected getSettingsPath(toolName?: string): string {
|
||||
const resolvedToolName = toolName || this.toolName
|
||||
return path.join(PROFILE_TOOLS_PATH, `${resolvedToolName}.settings.json`)
|
||||
|
||||
return path.join(
|
||||
PROFILE_TOOLS_PATH,
|
||||
this.toolkit,
|
||||
resolvedToolName,
|
||||
'settings.json'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,10 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
import { getPlatformName } from '@sdk/utils'
|
||||
import { PROFILE_TOOLS_PATH, TOOLKITS_PATH } from '@bridge/constants'
|
||||
import {
|
||||
PROFILE_TOOLS_PATH,
|
||||
TOOLS_PATH
|
||||
} from '@bridge/constants'
|
||||
|
||||
interface ToolConfig {
|
||||
tool_id: string
|
||||
@@ -28,7 +31,7 @@ export class ToolkitConfig {
|
||||
private static settingsCache = new Map<string, Record<string, unknown>>()
|
||||
|
||||
/**
|
||||
* Load tool configuration from bridges/toolkits directory
|
||||
* Load tool configuration from the flat tools structure.
|
||||
* @param toolkitName - The toolkit name (e.g., 'video_streaming')
|
||||
* @param toolName - Name of the tool (e.g., 'ffmpeg')
|
||||
*/
|
||||
@@ -37,7 +40,7 @@ export class ToolkitConfig {
|
||||
|
||||
// Load toolkit config if not cached
|
||||
if (!this.configCache.has(cacheKey)) {
|
||||
const configPath = join(TOOLKITS_PATH, toolkitName, 'toolkit.json')
|
||||
const configPath = join(TOOLS_PATH, toolkitName, 'toolkit.json')
|
||||
const configContent = readFileSync(configPath, 'utf-8')
|
||||
const config = JSON.parse(configContent) as ToolkitConfigData
|
||||
|
||||
@@ -45,15 +48,8 @@ export class ToolkitConfig {
|
||||
}
|
||||
|
||||
const toolkitConfig = this.configCache.get(cacheKey)!
|
||||
const toolConfigPath = join(
|
||||
TOOLKITS_PATH,
|
||||
toolkitName,
|
||||
'tools',
|
||||
`${toolName}.tool.json`
|
||||
)
|
||||
const toolConfigPath = join(TOOLS_PATH, toolkitName, toolName, 'tool.json')
|
||||
|
||||
// toolkit.json remains the discovery surface for agent/runtime registry flows,
|
||||
// but direct skill-side tool usage should still work when the tool manifest exists.
|
||||
if (!toolkitConfig.tools.includes(toolName) && !existsSync(toolConfigPath)) {
|
||||
throw new Error(
|
||||
`Tool '${toolName}' not found in toolkit '${toolkitConfig.name}'`
|
||||
@@ -82,7 +78,12 @@ export class ToolkitConfig {
|
||||
return this.settingsCache.get(cacheKey) || {}
|
||||
}
|
||||
|
||||
const settingsPath = join(PROFILE_TOOLS_PATH, `${toolName}.settings.json`)
|
||||
const settingsPath = join(
|
||||
PROFILE_TOOLS_PATH,
|
||||
toolkitName,
|
||||
toolName,
|
||||
'settings.json'
|
||||
)
|
||||
const settingsDir = dirname(settingsPath)
|
||||
|
||||
mkdirSync(settingsDir, { recursive: true })
|
||||
|
||||
@@ -8,6 +8,11 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
|
||||
import type { Tool } from '@sdk/base-tool'
|
||||
import {
|
||||
TOOLS_PATH
|
||||
} from '@bridge/constants'
|
||||
|
||||
interface ToolRuntimeCliInput {
|
||||
toolkitId: string
|
||||
toolId: string
|
||||
@@ -52,39 +57,25 @@ const parseArgs = (): ToolRuntimeCliInput => {
|
||||
}
|
||||
}
|
||||
|
||||
const resolveToolModulePath = async (
|
||||
const resolveToolModulePath = (
|
||||
toolkitId: string,
|
||||
toolId: string
|
||||
): Promise<string | null> => {
|
||||
const runtimeDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const toolsRoot = path.join(runtimeDir, 'sdk', 'tools')
|
||||
if (!fs.existsSync(toolsRoot)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const directPath = path.join(toolsRoot, toolId, 'index.ts')
|
||||
if (fs.existsSync(directPath)) {
|
||||
return directPath
|
||||
}
|
||||
|
||||
const normalizedToolId = normalizeName(toolId)
|
||||
const entries = await fs.promises.readdir(toolsRoot, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue
|
||||
if (normalizeName(entry.name) === normalizedToolId) {
|
||||
const candidate = path.join(toolsRoot, entry.name, 'index.ts')
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
): string | null => {
|
||||
const flatBuiltInToolPath = path.join(
|
||||
TOOLS_PATH,
|
||||
toolkitId,
|
||||
toolId,
|
||||
'src',
|
||||
'nodejs',
|
||||
'index.ts'
|
||||
)
|
||||
if (fs.existsSync(flatBuiltInToolPath)) {
|
||||
return flatBuiltInToolPath
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizeName = (value: string): string => {
|
||||
return value.replace(/[^a-z0-9]/gi, '').toLowerCase()
|
||||
}
|
||||
|
||||
const setProjectCwd = (): void => {
|
||||
const runtimeDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const projectRoot = path.join(runtimeDir, '..', '..', '..')
|
||||
@@ -97,12 +88,14 @@ const run = async (): Promise<void> => {
|
||||
try {
|
||||
setProjectCwd()
|
||||
const input = parseArgs()
|
||||
const toolModulePath = await resolveToolModulePath(input.toolId)
|
||||
const toolModulePath = resolveToolModulePath(
|
||||
input.toolkitId,
|
||||
input.toolId
|
||||
)
|
||||
if (!toolModulePath) {
|
||||
throw new Error(`Tool module not found for ${input.toolId}.`)
|
||||
}
|
||||
|
||||
const { Tool } = await import('@sdk/base-tool')
|
||||
const toolManagerModule = await import('@sdk/tool-manager')
|
||||
const ToolManager = toolManagerModule.default
|
||||
const isMissingToolSettingsError =
|
||||
@@ -113,11 +106,11 @@ const run = async (): Promise<void> => {
|
||||
throw new Error(`Tool ${input.toolId} has no default export.`)
|
||||
}
|
||||
|
||||
let toolInstance: InstanceType<typeof Tool>
|
||||
let toolInstance: Tool
|
||||
try {
|
||||
toolInstance = (await ToolManager.initTool(
|
||||
ToolClass as new () => InstanceType<typeof Tool>
|
||||
)) as InstanceType<typeof Tool>
|
||||
ToolClass as new () => Tool
|
||||
)) as Tool
|
||||
} catch (error) {
|
||||
if (isMissingToolSettingsError(error)) {
|
||||
process.stdout.write(
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"@aurora/*": ["../../aurora/dist/*"],
|
||||
"@server/*": ["../../server/src/*"],
|
||||
"@bridge/*": ["./src/*"],
|
||||
"@sdk/*": ["./src/sdk/*"]
|
||||
"@sdk/*": ["./src/sdk/*"],
|
||||
"@tools/*": ["../../tools/*/src/nodejs/index.ts", "../../tools/*.ts"]
|
||||
},
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"declaration": true
|
||||
|
||||
@@ -40,8 +40,10 @@ PROFILE_MEMORY_PATH = os.path.join(LEON_PROFILE_PATH, "memory")
|
||||
PROFILE_MEMORY_DB_PATH = os.path.join(PROFILE_MEMORY_PATH, "index.sqlite")
|
||||
PROFILE_SKILLS_PATH = os.path.join(LEON_PROFILE_PATH, "skills")
|
||||
PROFILE_TOOLS_PATH = os.path.join(LEON_PROFILE_PATH, "tools")
|
||||
PROFILE_DISABLED_PATH = os.path.join(LEON_PROFILE_PATH, "disabled.json")
|
||||
|
||||
SKILLS_ROOT_PATH = os.path.join(CODEBASE_PATH, "skills")
|
||||
TOOLS_PATH = os.path.join(CODEBASE_PATH, "tools")
|
||||
BIN_PATH = os.path.join(LEON_HOME_PATH, "bin")
|
||||
BRIDGES_PATH = os.path.join(CODEBASE_PATH, "bridges")
|
||||
|
||||
@@ -50,9 +52,7 @@ NVIDIA_LIBS_PATH = os.path.join(BIN_PATH, "nvidia")
|
||||
PYTORCH_PATH = os.path.join(BIN_PATH, "pytorch")
|
||||
PYTORCH_TORCH_PATH = os.path.join(PYTORCH_PATH, "torch")
|
||||
|
||||
TOOLKITS_PATH = os.path.join(BRIDGES_PATH, "toolkits")
|
||||
|
||||
SKILL_PATH = os.path.join(SKILLS_ROOT_PATH, INTENT_OBJECT["skill_name"])
|
||||
SKILL_PATH = os.path.dirname(INTENT_OBJECT["skill_config_path"])
|
||||
|
||||
SKILLS_PATH = SKILLS_ROOT_PATH
|
||||
|
||||
|
||||
+42
-14
@@ -2,9 +2,9 @@ import sys
|
||||
import os
|
||||
import inspect
|
||||
from traceback import print_exc
|
||||
from importlib import import_module
|
||||
from importlib import util
|
||||
|
||||
from constants import INTENT_OBJECT, PROFILE_SKILLS_PATH
|
||||
from constants import INTENT_OBJECT, SKILL_PATH
|
||||
from sdk.params_helper import ParamsHelper
|
||||
|
||||
|
||||
@@ -26,19 +26,34 @@ def resolve_action_function(skill_action_module):
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
skill_vendor_path = os.path.abspath(
|
||||
def get_skill_venv_site_packages_path():
|
||||
venv_path = os.path.join(SKILL_PATH, 'src', '.venv')
|
||||
candidates = [
|
||||
os.path.join(
|
||||
PROFILE_SKILLS_PATH,
|
||||
INTENT_OBJECT['skill_name'],
|
||||
'.runtime',
|
||||
'vendor'
|
||||
venv_path,
|
||||
'Lib',
|
||||
'site-packages'
|
||||
),
|
||||
os.path.join(
|
||||
venv_path,
|
||||
'lib',
|
||||
f'python{sys.version_info.major}.{sys.version_info.minor}',
|
||||
'site-packages'
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
if os.path.isdir(skill_vendor_path):
|
||||
# Skill-specific Python dependencies are vendored at install time.
|
||||
sys.path.insert(0, skill_vendor_path)
|
||||
for candidate in candidates:
|
||||
if os.path.isdir(candidate):
|
||||
return os.path.abspath(candidate)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
skill_site_packages_path = get_skill_venv_site_packages_path()
|
||||
|
||||
if skill_site_packages_path:
|
||||
sys.path.insert(0, skill_site_packages_path)
|
||||
|
||||
params = {
|
||||
'lang': INTENT_OBJECT['lang'],
|
||||
@@ -57,13 +72,26 @@ def main():
|
||||
|
||||
try:
|
||||
sys.path.append('.')
|
||||
sys.path.insert(0, os.path.dirname(SKILL_PATH))
|
||||
|
||||
skill_action_module = import_module(
|
||||
action_path = os.path.join(
|
||||
SKILL_PATH,
|
||||
'src',
|
||||
'actions',
|
||||
INTENT_OBJECT['action_name'] + '.py'
|
||||
)
|
||||
spec = util.spec_from_file_location(
|
||||
'skills.'
|
||||
+ INTENT_OBJECT['skill_name']
|
||||
+ '.src.actions.'
|
||||
+ INTENT_OBJECT['action_name']
|
||||
+ INTENT_OBJECT['action_name'],
|
||||
action_path
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f'Cannot load action module from "{action_path}"')
|
||||
|
||||
skill_action_module = util.module_from_spec(spec)
|
||||
spec.loader.exec_module(skill_action_module)
|
||||
|
||||
run_function = resolve_action_function(skill_action_module)
|
||||
if not callable(run_function):
|
||||
|
||||
@@ -21,7 +21,6 @@ from ..constants import (
|
||||
NVIDIA_LIBS_PATH,
|
||||
PROFILE_TOOLS_PATH,
|
||||
PYTORCH_TORCH_PATH,
|
||||
TOOLKITS_PATH,
|
||||
)
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -104,7 +103,10 @@ class BaseTool(ABC):
|
||||
|
||||
def _get_settings_path(self, tool_name: Optional[str] = None) -> str:
|
||||
resolved_tool_name = tool_name or self.tool_name
|
||||
return os.path.join(PROFILE_TOOLS_PATH, f"{resolved_tool_name}.settings.json")
|
||||
|
||||
return os.path.join(
|
||||
PROFILE_TOOLS_PATH, self.toolkit, resolved_tool_name, "settings.json"
|
||||
)
|
||||
|
||||
def _check_required_settings(self, tool_name: Optional[str] = None) -> None:
|
||||
if not self.required_settings:
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from ..constants import PROFILE_TOOLS_PATH, TOOLKITS_PATH
|
||||
from ..constants import PROFILE_TOOLS_PATH, TOOLS_PATH
|
||||
from .utils import get_platform_name
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class ToolkitConfig:
|
||||
@classmethod
|
||||
def load(cls, toolkit_name: str, tool_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Load tool configuration from bridges/toolkits directory
|
||||
Load tool configuration from the flat tools structure.
|
||||
|
||||
Args:
|
||||
toolkit_name: The toolkit name (e.g., 'video_streaming')
|
||||
@@ -25,7 +25,7 @@ class ToolkitConfig:
|
||||
|
||||
# Load toolkit config if not cached
|
||||
if cache_key not in cls._config_cache:
|
||||
config_path = os.path.join(TOOLKITS_PATH, toolkit_name, "toolkit.json")
|
||||
config_path = os.path.join(TOOLS_PATH, toolkit_name, "toolkit.json")
|
||||
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
@@ -40,13 +40,8 @@ class ToolkitConfig:
|
||||
toolkit_config = cls._config_cache[cache_key]
|
||||
tools_list = toolkit_config.get("tools", [])
|
||||
|
||||
tool_config_path = os.path.join(
|
||||
TOOLKITS_PATH, toolkit_name, "tools", f"{tool_name}.tool.json"
|
||||
)
|
||||
tool_config_path = os.path.join(TOOLS_PATH, toolkit_name, tool_name, "tool.json")
|
||||
|
||||
# toolkit.json remains the discovery surface for agent/runtime registry
|
||||
# flows, but direct skill-side tool usage should still work when the
|
||||
# tool manifest exists.
|
||||
if tool_name not in tools_list and not os.path.exists(tool_config_path):
|
||||
toolkit_name_display = toolkit_config.get("name", "unknown")
|
||||
raise Exception(
|
||||
@@ -82,7 +77,9 @@ class ToolkitConfig:
|
||||
if cache_key in cls._settings_cache:
|
||||
return cls._settings_cache[cache_key]
|
||||
|
||||
settings_path = os.path.join(PROFILE_TOOLS_PATH, f"{tool_name}.settings.json")
|
||||
settings_path = os.path.join(
|
||||
PROFILE_TOOLS_PATH, toolkit_name, tool_name, "settings.json"
|
||||
)
|
||||
settings_dir = os.path.dirname(settings_path)
|
||||
os.makedirs(settings_dir, exist_ok=True)
|
||||
|
||||
|
||||
+1
-2
@@ -6,8 +6,7 @@
|
||||
".git",
|
||||
"node_modules",
|
||||
"server/src/tmp",
|
||||
"server/dist",
|
||||
"bridges/toolkits"
|
||||
"server/dist"
|
||||
],
|
||||
"exec": "node scripts/run-with-managed-node.js node_modules/typescript/bin/tsc --noEmit -p tsconfig.json && node scripts/run-with-managed-node.js node_modules/tsx/dist/cli.mjs server/src/index.ts"
|
||||
}
|
||||
|
||||
+1
-2
@@ -6,8 +6,7 @@
|
||||
".git",
|
||||
"node_modules",
|
||||
"server/src/tmp",
|
||||
"server/dist",
|
||||
"bridges/toolkits"
|
||||
"server/dist"
|
||||
],
|
||||
"exec": "node scripts/run-with-managed-node.js node_modules/tsx/dist/cli.mjs server/src/index.ts"
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@
|
||||
"python-bridge": "tsx scripts/run-python-bridge.js server/src/intent-object.sample.json",
|
||||
"train": "tsx scripts/train/run-train.js",
|
||||
"prepare-release": "tsx scripts/release/prepare-release.js",
|
||||
"sync-skill-deps": "tsx scripts/sync-skill-deps.js",
|
||||
"check": "tsx scripts/check.js",
|
||||
"kill": "tsx scripts/kill.js"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import dotenv from 'dotenv'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { StringHelper } from '@/helpers/string-helper'
|
||||
import { DotEnvHelper } from '@/helpers/dotenv-helper'
|
||||
import { ProfileHelper } from '@/helpers/profile-helper'
|
||||
import { PROFILE_DOT_ENV_PATH } from '@/constants'
|
||||
|
||||
dotenv.config({ path: PROFILE_DOT_ENV_PATH })
|
||||
@@ -25,7 +25,7 @@ const generateHTTPAPIKey = () =>
|
||||
shasum.update(str)
|
||||
const sha1 = shasum.digest('hex')
|
||||
|
||||
await DotEnvHelper.updateVariable(envVarKey, sha1)
|
||||
await ProfileHelper.updateDotEnvVariable(envVarKey, sha1)
|
||||
LogHelper.success('HTTP API key generated')
|
||||
|
||||
resolve()
|
||||
|
||||
+3
-3
@@ -10,11 +10,11 @@ const globs = [
|
||||
'aurora/src/**/*.{ts,tsx,js,jsx}',
|
||||
// TODO: deal with it once handling new hotword
|
||||
// '"hotword/index.{ts,js}"',
|
||||
// TODO: put it back once tests have been reintroduced into skills
|
||||
// '"skills/**/*.js"',
|
||||
'skills/**/*.{ts,js}',
|
||||
'scripts/**/*.{ts,js}',
|
||||
'server/src/**/*.{ts,js}',
|
||||
'test/**/*.{ts,js}'
|
||||
'test/**/*.{ts,js}',
|
||||
'tools/**/*.ts'
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,9 +19,9 @@ You must create a new tool for `{TOOL_ALIAS_NAME}`. {TOOL_DESCRIPTION}
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
- Tools are located under `bridges/nodejs/src/sdk/tools` and `bridges/python/src/sdk/tools`.
|
||||
- Tools are located under `tools/{TOOL_TOOLKIT_NAME}/{TOOL_NAME}/src/nodejs` and `tools/{TOOL_TOOLKIT_NAME}/{TOOL_NAME}/src/python`.
|
||||
- The tool must belong to the `{TOOL_TOOLKIT_NAME}` toolkit.
|
||||
- Fill the `bridges/toolkits/{TOOL_TOOLKIT_NAME}/tools/{TOOL_NAME}.tool.json` file. You must provide the description, binaries, resources, function definitions by following the OpenAI function-calling standard, etc. Create the file is not created yet.
|
||||
- Fill the `tools/{TOOL_TOOLKIT_NAME}/{TOOL_NAME}/tool.json` file. You must provide the description, binaries, resources, function definitions by following the OpenAI function-calling standard, etc. Create the file is not created yet.
|
||||
- You must create the tool with the TypeScript SDK and the Python SDK. The business logic must literally be the same. Start by writting the TypeScript code and then translate/convert to Python for the Python tool.
|
||||
- Tool file names must be `{TOOL_TS_FILE_NAME}` and `{TOOL_PYTHON_FILE_NAME}`.
|
||||
- You must reuse the classes and functions provided by the SDK (network, settings, etc.). You will find them in the SDK folder.
|
||||
|
||||
@@ -1,160 +1,27 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import execa from 'execa'
|
||||
|
||||
import {
|
||||
PNPM_RUNTIME_BIN_PATH,
|
||||
PYTHON_RUNTIME_BIN_PATH,
|
||||
UV_RUNTIME_BIN_PATH
|
||||
} from '@/constants'
|
||||
import { RuntimeHelper } from '@/helpers/runtime-helper'
|
||||
|
||||
import { getPyprojectDependencies } from '../setup-python-project-env'
|
||||
|
||||
const SYNC_STAMP_FILE_NAME = '.last-skill-deps-sync'
|
||||
|
||||
/**
|
||||
* Stamp files let setup stay cheap on repeated boots while still re-syncing as
|
||||
* soon as a skill dependency manifest changes.
|
||||
*/
|
||||
const getSyncStampPath = (skillPath) => {
|
||||
return path.join(
|
||||
RuntimeHelper.getSkillRuntimePath(skillPath),
|
||||
SYNC_STAMP_FILE_NAME
|
||||
)
|
||||
}
|
||||
|
||||
const isFileEmpty = async (filePath) => {
|
||||
const content = await fs.promises.readFile(filePath, 'utf8')
|
||||
return content.trim() === ''
|
||||
}
|
||||
|
||||
const isSyncCurrent = async (skillPath, manifestPath) => {
|
||||
const stampPath = getSyncStampPath(skillPath)
|
||||
|
||||
if (!fs.existsSync(stampPath) || !fs.existsSync(manifestPath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const [stampStat, manifestStat] = await Promise.all([
|
||||
fs.promises.stat(stampPath),
|
||||
fs.promises.stat(manifestPath)
|
||||
])
|
||||
|
||||
return manifestStat.mtimeMs <= stampStat.mtimeMs
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the skill as synced after a successful dependency install/update.
|
||||
*/
|
||||
const markSkillDependenciesAsSynced = async (skillPath) => {
|
||||
await fs.promises.mkdir(RuntimeHelper.getSkillRuntimePath(skillPath), {
|
||||
recursive: true
|
||||
})
|
||||
await fs.promises.writeFile(getSyncStampPath(skillPath), `${Date.now()}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Node skill dependencies stay inside the skill runtime directory so they do
|
||||
* not leak into Leon core dependencies or other skills.
|
||||
*/
|
||||
const syncNodejsSkillDependencies = async (skillFriendlyName, skillPath) => {
|
||||
const skillSRCPath = path.join(skillPath, 'src')
|
||||
const packageJSONPath = path.join(skillSRCPath, 'package.json')
|
||||
const runtimePath = RuntimeHelper.getSkillRuntimePath(skillPath)
|
||||
const nodeModulesPath =
|
||||
RuntimeHelper.getNodejsSkillRuntimeNodeModulesPath(skillPath)
|
||||
const runtimePackageJSONPath = path.join(runtimePath, 'package.json')
|
||||
|
||||
if (!fs.existsSync(packageJSONPath) || (await isFileEmpty(packageJSONPath))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (await isSyncCurrent(skillPath, packageJSONPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(runtimePath, { recursive: true })
|
||||
await fs.promises.rm(nodeModulesPath, { recursive: true, force: true })
|
||||
await fs.promises.copyFile(packageJSONPath, runtimePackageJSONPath)
|
||||
|
||||
// Install from the runtime directory itself so pnpm does not create importer
|
||||
// metadata under the skill source tree.
|
||||
await execa(PNPM_RUNTIME_BIN_PATH, [
|
||||
'install',
|
||||
'--ignore-workspace',
|
||||
'--lockfile=false'
|
||||
], {
|
||||
cwd: runtimePath,
|
||||
env: RuntimeHelper.getManagedNodeEnvironment()
|
||||
})
|
||||
|
||||
await markSkillDependenciesAsSynced(skillPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor Python dependencies into the skill itself so each skill remains
|
||||
* portable and isolated from the bridge-wide Python environment.
|
||||
*/
|
||||
const installPythonDependencies = async (dependencies, vendorPath) => {
|
||||
await execa(UV_RUNTIME_BIN_PATH, [
|
||||
'pip',
|
||||
'install',
|
||||
'--python',
|
||||
PYTHON_RUNTIME_BIN_PATH,
|
||||
'--target',
|
||||
vendorPath,
|
||||
...dependencies
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Python skill dependencies are always re-installed into a fresh vendor
|
||||
* directory so stale packages do not survive a version update.
|
||||
*/
|
||||
const syncPythonSkillDependencies = async (skillFriendlyName, skillPath) => {
|
||||
const skillSRCPath = path.join(skillPath, 'src')
|
||||
const manifestPath = path.join(skillSRCPath, 'pyproject.toml')
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (await isSyncCurrent(skillPath, manifestPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const runtimePath = RuntimeHelper.getSkillRuntimePath(skillPath)
|
||||
const vendorPath = RuntimeHelper.getPythonSkillRuntimeVendorPath(skillPath)
|
||||
const dependencies = await getPyprojectDependencies(skillSRCPath)
|
||||
|
||||
await fs.promises.mkdir(runtimePath, { recursive: true })
|
||||
await fs.promises.rm(vendorPath, { recursive: true, force: true })
|
||||
await fs.promises.mkdir(vendorPath, { recursive: true })
|
||||
|
||||
if (dependencies.length > 0) {
|
||||
await installPythonDependencies(dependencies, vendorPath)
|
||||
}
|
||||
|
||||
await markSkillDependenciesAsSynced(skillPath)
|
||||
}
|
||||
syncNodejsSourceDependencies,
|
||||
syncPythonSourceDependencies
|
||||
} from '../sync-source-dependencies'
|
||||
|
||||
/**
|
||||
* Sync skill dependencies only when a skill is installed, updated, or its
|
||||
* dependency manifest changes.
|
||||
*/
|
||||
export default async function syncSkillDependencies(
|
||||
skillFriendlyName,
|
||||
_skillFriendlyName,
|
||||
currentSkill
|
||||
) {
|
||||
const skillSRCPath = path.join(currentSkill.path, 'src')
|
||||
|
||||
if (currentSkill.bridge === 'nodejs') {
|
||||
await syncNodejsSkillDependencies(skillFriendlyName, currentSkill.path)
|
||||
await syncNodejsSourceDependencies(skillSRCPath)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (currentSkill.bridge === 'python') {
|
||||
await syncPythonSkillDependencies(skillFriendlyName, currentSkill.path)
|
||||
await syncPythonSourceDependencies(skillSRCPath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROFILE_TOOLS_PATH, TOOLS_PATH } from '@/constants'
|
||||
|
||||
import { createSetupStatus } from './setup-status'
|
||||
import {
|
||||
syncNodejsSourceDependencies,
|
||||
syncPythonSourceDependencies
|
||||
} from './sync-source-dependencies'
|
||||
|
||||
const NODEJS_SOURCE_PATH = path.join('src', 'nodejs')
|
||||
const PYTHON_SOURCE_PATH = path.join('src', 'python')
|
||||
|
||||
const getToolSourcePaths = async (toolsPath) => {
|
||||
if (!fs.existsSync(toolsPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const toolkitEntries = await fs.promises.readdir(toolsPath, {
|
||||
withFileTypes: true
|
||||
})
|
||||
const sourcePaths = []
|
||||
|
||||
for (const toolkitEntry of toolkitEntries) {
|
||||
if (!toolkitEntry.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const toolkitPath = path.join(toolsPath, toolkitEntry.name)
|
||||
const toolEntries = await fs.promises.readdir(toolkitPath, {
|
||||
withFileTypes: true
|
||||
})
|
||||
|
||||
for (const toolEntry of toolEntries) {
|
||||
if (!toolEntry.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const toolPath = path.join(toolkitPath, toolEntry.name)
|
||||
sourcePaths.push({
|
||||
bridge: 'nodejs',
|
||||
path: path.join(toolPath, NODEJS_SOURCE_PATH)
|
||||
})
|
||||
sourcePaths.push({
|
||||
bridge: 'python',
|
||||
path: path.join(toolPath, PYTHON_SOURCE_PATH)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return sourcePaths
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync tool dependencies next to each tool source folder.
|
||||
*/
|
||||
export default async function setupToolsDependencies() {
|
||||
const status = createSetupStatus('Setting up tool dependencies...').start()
|
||||
|
||||
try {
|
||||
const sourcePaths = [
|
||||
...(await getToolSourcePaths(TOOLS_PATH)),
|
||||
...(await getToolSourcePaths(PROFILE_TOOLS_PATH))
|
||||
]
|
||||
|
||||
for (const sourcePath of sourcePaths) {
|
||||
if (sourcePath.bridge === 'nodejs') {
|
||||
await syncNodejsSourceDependencies(sourcePath.path)
|
||||
} else {
|
||||
await syncPythonSourceDependencies(sourcePath.path)
|
||||
}
|
||||
}
|
||||
|
||||
status.succeed('Tool dependencies: ready')
|
||||
} catch (e) {
|
||||
status.fail('Failed to set up tool dependencies')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
PROFILE_CONTEXT_PATH,
|
||||
PROFILE_LOGS_PATH,
|
||||
PROFILE_MEMORY_PATH,
|
||||
PROFILE_DISABLED_PATH,
|
||||
PROFILE_SKILLS_PATH,
|
||||
PROFILE_TOOLS_PATH,
|
||||
TMP_PATH,
|
||||
@@ -36,6 +37,7 @@ import setupPython from './setup-python'
|
||||
import setupUV from './setup-uv'
|
||||
import setupNodejsBridgeEnv from './setup-nodejs-bridge-env'
|
||||
import setupPythonBridgeEnv from './setup-python-bridge-env'
|
||||
import setupToolsDependencies from './setup-tools-dependencies'
|
||||
import setupSkills from './setup-skills/setup-skills'
|
||||
import setupTCPServerEnv from './setup-tcp-server-env'
|
||||
import setupCMake from './setup-cmake'
|
||||
@@ -81,6 +83,13 @@ async function ensureLeonHomeStructure() {
|
||||
fs.promises.mkdir(PROFILE_TOOLS_PATH, { recursive: true })
|
||||
])
|
||||
|
||||
if (!fs.existsSync(PROFILE_DISABLED_PATH)) {
|
||||
await fs.promises.writeFile(
|
||||
PROFILE_DISABLED_PATH,
|
||||
JSON.stringify({ skills: [], tools: [] }, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
status.succeed('Leon home: ready')
|
||||
}
|
||||
|
||||
@@ -297,6 +306,8 @@ async function syncLLMSetupChoice(preferences) {
|
||||
await setupPythonBridgeEnv()
|
||||
currentStep = 'setupTCPServerEnv'
|
||||
await setupTCPServerEnv()
|
||||
currentStep = 'setupToolsDependencies'
|
||||
await setupToolsDependencies()
|
||||
currentStep = 'setupSkills'
|
||||
await setupSkills()
|
||||
if (!IS_GITHUB_ACTIONS) {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import execa from 'execa'
|
||||
|
||||
import {
|
||||
PNPM_RUNTIME_BIN_PATH,
|
||||
PYTHON_RUNTIME_BIN_PATH,
|
||||
UV_RUNTIME_BIN_PATH
|
||||
} from '@/constants'
|
||||
import { RuntimeHelper } from '@/helpers/runtime-helper'
|
||||
|
||||
import {
|
||||
getProjectVenvPythonPath,
|
||||
getPyprojectDependencies
|
||||
} from './setup-python-project-env'
|
||||
|
||||
const PACKAGE_JSON_FILE_NAME = 'package.json'
|
||||
const PYPROJECT_FILE_NAME = 'pyproject.toml'
|
||||
const SYNC_STAMP_FILE_NAME = '.last-source-deps-sync'
|
||||
const NODE_MODULES_DIR_NAME = 'node_modules'
|
||||
const VENV_DIR_NAME = '.venv'
|
||||
|
||||
const isFileEmpty = async (filePath) => {
|
||||
const content = await fs.promises.readFile(filePath, 'utf8')
|
||||
|
||||
return content.trim() === ''
|
||||
}
|
||||
|
||||
const getSyncStampPath = (sourcePath) => {
|
||||
return path.join(sourcePath, SYNC_STAMP_FILE_NAME)
|
||||
}
|
||||
|
||||
const isSyncCurrent = async (manifestPath, stampPath, dependencyPath) => {
|
||||
if (
|
||||
!fs.existsSync(stampPath) ||
|
||||
!fs.existsSync(manifestPath) ||
|
||||
!fs.existsSync(dependencyPath)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const [manifestStat, stampStat] = await Promise.all([
|
||||
fs.promises.stat(manifestPath),
|
||||
fs.promises.stat(stampPath)
|
||||
])
|
||||
|
||||
return manifestStat.mtimeMs <= stampStat.mtimeMs
|
||||
}
|
||||
|
||||
const markSourceDependenciesAsSynced = async (sourcePath) => {
|
||||
await fs.promises.writeFile(getSyncStampPath(sourcePath), `${Date.now()}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Node.js dependencies next to the source that declares them.
|
||||
*/
|
||||
export const syncNodejsSourceDependencies = async (sourcePath) => {
|
||||
const packageJSONPath = path.join(sourcePath, PACKAGE_JSON_FILE_NAME)
|
||||
const nodeModulesPath = path.join(sourcePath, NODE_MODULES_DIR_NAME)
|
||||
const stampPath = getSyncStampPath(sourcePath)
|
||||
|
||||
if (!fs.existsSync(packageJSONPath) || (await isFileEmpty(packageJSONPath))) {
|
||||
return
|
||||
}
|
||||
|
||||
if (await isSyncCurrent(packageJSONPath, stampPath, nodeModulesPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
await fs.promises.rm(nodeModulesPath, { recursive: true, force: true })
|
||||
|
||||
await execa(PNPM_RUNTIME_BIN_PATH, [
|
||||
'install',
|
||||
'--ignore-workspace',
|
||||
'--lockfile=false'
|
||||
], {
|
||||
cwd: sourcePath,
|
||||
env: RuntimeHelper.getManagedNodeEnvironment()
|
||||
})
|
||||
|
||||
await markSourceDependenciesAsSynced(sourcePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Python dependencies into a .venv next to the source that declares them.
|
||||
*/
|
||||
export const syncPythonSourceDependencies = async (sourcePath) => {
|
||||
const manifestPath = path.join(sourcePath, PYPROJECT_FILE_NAME)
|
||||
const venvPath = path.join(sourcePath, VENV_DIR_NAME)
|
||||
const stampPath = getSyncStampPath(sourcePath)
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
await isSyncCurrent(
|
||||
manifestPath,
|
||||
stampPath,
|
||||
getProjectVenvPythonPath(sourcePath)
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const dependencies = await getPyprojectDependencies(sourcePath)
|
||||
|
||||
await fs.promises.rm(venvPath, { recursive: true, force: true })
|
||||
await execa(UV_RUNTIME_BIN_PATH, [
|
||||
'venv',
|
||||
'--python',
|
||||
PYTHON_RUNTIME_BIN_PATH,
|
||||
venvPath
|
||||
], { cwd: sourcePath })
|
||||
|
||||
if (dependencies.length > 0) {
|
||||
await execa(UV_RUNTIME_BIN_PATH, [
|
||||
'pip',
|
||||
'install',
|
||||
'--python',
|
||||
getProjectVenvPythonPath(sourcePath),
|
||||
...dependencies
|
||||
], { cwd: sourcePath })
|
||||
}
|
||||
|
||||
await markSourceDependenciesAsSynced(sourcePath)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { createListResult } from '@/commands/built-in-command-renderer'
|
||||
import { CONFIG_STATE } from '@/core/config-states/config-state'
|
||||
import { LLMProviders } from '@/core/llm-manager/types'
|
||||
import { DotEnvHelper } from '@/helpers/dotenv-helper'
|
||||
import { ProfileHelper } from '@/helpers/profile-helper'
|
||||
|
||||
const API_KEY_PARAMETER_NAME = 'api_key'
|
||||
const API_KEY_INPUT_PLACEHOLDER = 'Paste API key here'
|
||||
@@ -262,7 +262,7 @@ export class ModelCommand extends BuiltInCommand {
|
||||
}
|
||||
|
||||
process.env[apiKeyEnv] = apiKey
|
||||
await DotEnvHelper.updateVariable(apiKeyEnv, apiKey)
|
||||
await ProfileHelper.updateDotEnvVariable(apiKeyEnv, apiKey)
|
||||
await CONFIG_STATE.getModelState().setUnifiedTarget(configuredTarget)
|
||||
|
||||
return {
|
||||
|
||||
@@ -60,6 +60,7 @@ export const PROFILE_MEMORY_PATH = path.join(LEON_PROFILE_PATH, 'memory')
|
||||
export const PROFILE_LOGS_PATH = path.join(LEON_PROFILE_PATH, 'logs')
|
||||
export const PROFILE_SKILLS_PATH = path.join(LEON_PROFILE_PATH, 'skills')
|
||||
export const PROFILE_TOOLS_PATH = path.join(LEON_PROFILE_PATH, 'tools')
|
||||
export const PROFILE_DISABLED_PATH = path.join(LEON_PROFILE_PATH, 'disabled.json')
|
||||
export const PROFILE_CONVERSATION_LOG_PATH = path.join(
|
||||
LEON_PROFILE_PATH,
|
||||
'conversation_log.json'
|
||||
@@ -72,6 +73,7 @@ export const PNPM_INSTALL_PATH = path.join(BIN_PATH, 'pnpm')
|
||||
export const PYTHON_INSTALL_PATH = path.join(BIN_PATH, 'python')
|
||||
export const UV_INSTALL_PATH = path.join(BIN_PATH, 'uv')
|
||||
export const SKILLS_PATH = path.join(CODEBASE_PATH, 'skills')
|
||||
export const TOOLS_PATH = path.join(CODEBASE_PATH, 'tools')
|
||||
export const GLOBAL_CORE_PATH = path.join(CODEBASE_PATH, 'core')
|
||||
export const GLOBAL_DATA_PATH = path.join(GLOBAL_CORE_PATH, 'data')
|
||||
export const PROFILE_MEMORY_DB_PATH = path.join(
|
||||
@@ -319,7 +321,6 @@ export const PYTORCH_VERSION = PYTORCH_VERSIONS.torch
|
||||
*/
|
||||
export const BINARIES_FOLDER_NAME = SystemHelper.getBinariesFolderName()
|
||||
export const BRIDGES_PATH = path.join(CODEBASE_PATH, 'bridges')
|
||||
export const TOOLKITS_PATH = path.join(BRIDGES_PATH, 'toolkits')
|
||||
export const NODEJS_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'nodejs')
|
||||
export const PYTHON_BRIDGE_ROOT_PATH = path.join(BRIDGES_PATH, 'python')
|
||||
export const PYTHON_TCP_SERVER_ROOT_PATH = path.join(
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
CONFIG_STATE_EVENT_EMITTER,
|
||||
MODEL_CONFIGURATION_UPDATED_EVENT
|
||||
} from '@/core/config-states/config-state-event-emitter'
|
||||
import { DotEnvHelper } from '@/helpers/dotenv-helper'
|
||||
import { ProfileHelper } from '@/helpers/profile-helper'
|
||||
import { FileHelper } from '@/helpers/file-helper'
|
||||
|
||||
const GLOBAL_LLM_ENV_KEY = 'LEON_LLM'
|
||||
@@ -214,7 +214,10 @@ export class ModelState {
|
||||
|
||||
process.env[GLOBAL_LLM_ENV_KEY] = normalizedRawTarget
|
||||
|
||||
await DotEnvHelper.updateVariable(GLOBAL_LLM_ENV_KEY, normalizedRawTarget)
|
||||
await ProfileHelper.updateDotEnvVariable(
|
||||
GLOBAL_LLM_ENV_KEY,
|
||||
normalizedRawTarget
|
||||
)
|
||||
|
||||
CONFIG_STATE_EVENT_EMITTER.emit(MODEL_CONFIGURATION_UPDATED_EVENT, {
|
||||
workflowTarget: this.workflowTarget,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
CONFIG_STATE_EVENT_EMITTER,
|
||||
MOOD_CONFIGURATION_UPDATED_EVENT
|
||||
} from '@/core/config-states/config-state-event-emitter'
|
||||
import { DotEnvHelper } from '@/helpers/dotenv-helper'
|
||||
import { ProfileHelper } from '@/helpers/profile-helper'
|
||||
import { Moods } from '@/types'
|
||||
|
||||
const DEFAULT_CONFIGURED_MOOD = 'auto'
|
||||
@@ -114,7 +114,10 @@ export class MoodState {
|
||||
: normalizedConfiguredMood
|
||||
process.env[MOOD_ENV_KEY] = normalizedConfiguredMood
|
||||
|
||||
await DotEnvHelper.updateVariable(MOOD_ENV_KEY, normalizedConfiguredMood)
|
||||
await ProfileHelper.updateDotEnvVariable(
|
||||
MOOD_ENV_KEY,
|
||||
normalizedConfiguredMood
|
||||
)
|
||||
|
||||
CONFIG_STATE_EVENT_EMITTER.emit(MOOD_CONFIGURATION_UPDATED_EVENT, {
|
||||
configuredMood: this.configuredMood,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LEON_ROUTING_MODE } from '@/constants'
|
||||
import { DotEnvHelper } from '@/helpers/dotenv-helper'
|
||||
import { ProfileHelper } from '@/helpers/profile-helper'
|
||||
import { RoutingMode } from '@/types'
|
||||
|
||||
const DEFAULT_ROUTING_MODE = RoutingMode.Smart
|
||||
@@ -38,7 +38,7 @@ export class RoutingModeState {
|
||||
this.routingMode = normalizedRoutingMode
|
||||
process.env[ROUTING_MODE_ENV_KEY] = normalizedRoutingMode
|
||||
|
||||
await DotEnvHelper.updateVariable(
|
||||
await ProfileHelper.updateDotEnvVariable(
|
||||
ROUTING_MODE_ENV_KEY,
|
||||
normalizedRoutingMode
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
SERVER_CORE_PATH,
|
||||
SKILLS_PATH,
|
||||
TMP_PATH,
|
||||
TOOLKITS_PATH
|
||||
TOOLS_PATH
|
||||
} from '@/constants'
|
||||
import { DateHelper } from '@/helpers/date-helper'
|
||||
import { ContextFile } from '@/core/context-manager/context-file'
|
||||
@@ -33,7 +33,7 @@ export class HomeContextFile extends ContextFile {
|
||||
`- Generated at: ${DateHelper.getDateTime()}`,
|
||||
`- Codebase path: ${codebasePath}`,
|
||||
`- Skills path: ${SKILLS_PATH}`,
|
||||
`- Toolkits path: ${TOOLKITS_PATH}`,
|
||||
`- Tools path: ${TOOLS_PATH}`,
|
||||
`- Global data path: ${GLOBAL_DATA_PATH}`,
|
||||
`- Models path: ${MODELS_PATH}`,
|
||||
`- Context path: ${PROFILE_CONTEXT_PATH}`,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { Json as NodeJQJson } from 'node-jq/lib/options'
|
||||
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import {
|
||||
CODEBASE_PATH,
|
||||
NODE_RUNTIME_BIN_PATH,
|
||||
NODEJS_BRIDGE_TOOL_RUNTIME_SRC_PATH,
|
||||
NODEJS_BRIDGE_ROOT_PATH,
|
||||
@@ -668,7 +669,10 @@ export default class ToolExecutor {
|
||||
{
|
||||
cwd: NODEJS_BRIDGE_ROOT_PATH,
|
||||
maxBuffer: 1_024 * 1_024 * 10,
|
||||
env: process.env
|
||||
env: {
|
||||
...process.env,
|
||||
LEON_CODEBASE_PATH: CODEBASE_PATH
|
||||
}
|
||||
}
|
||||
)
|
||||
const output = stdout ? stdout.toString().trim() : ''
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { TOOLKITS_PATH } from '@/constants'
|
||||
import { TOOLS_PATH } from '@/constants'
|
||||
import { LogHelper } from '@/helpers/log-helper'
|
||||
import { ProfileHelper } from '@/helpers/profile-helper'
|
||||
|
||||
interface ToolkitToolDefinition {
|
||||
tool_id: string
|
||||
@@ -255,100 +256,13 @@ export default class ToolkitRegistry {
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = await fs.promises.readdir(TOOLKITS_PATH, {
|
||||
withFileTypes: true
|
||||
})
|
||||
const toolkitsById = new Map<string, ToolkitDefinition>()
|
||||
|
||||
const toolkits: ToolkitDefinition[] = []
|
||||
await this.loadBuiltInToolkits(toolkitsById)
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const toolkitId = entry.name
|
||||
const toolkitPath = path.join(TOOLKITS_PATH, toolkitId)
|
||||
const toolkitConfigPath = path.join(toolkitPath, 'toolkit.json')
|
||||
|
||||
if (!fs.existsSync(toolkitConfigPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const toolkitConfigRaw = await fs.promises.readFile(
|
||||
toolkitConfigPath,
|
||||
'utf-8'
|
||||
)
|
||||
const toolkitConfig = JSON.parse(toolkitConfigRaw) as {
|
||||
name: string
|
||||
description: string
|
||||
icon_name: string
|
||||
context_files?: string[]
|
||||
tools?: string[]
|
||||
}
|
||||
|
||||
if (!toolkitConfig.tools || toolkitConfig.tools.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const contextFiles = Array.isArray(toolkitConfig.context_files)
|
||||
? [
|
||||
...new Set(
|
||||
toolkitConfig.context_files
|
||||
.map((contextFile) =>
|
||||
this.normalizeContextFilename(contextFile)
|
||||
)
|
||||
.filter((contextFile): contextFile is string =>
|
||||
Boolean(contextFile)
|
||||
)
|
||||
)
|
||||
]
|
||||
: []
|
||||
|
||||
const toolkitTools: Record<string, ToolkitToolDefinition> = {}
|
||||
for (const toolId of toolkitConfig.tools) {
|
||||
const toolConfigPath = path.join(
|
||||
TOOLKITS_PATH,
|
||||
toolkitId,
|
||||
'tools',
|
||||
`${toolId}.tool.json`
|
||||
)
|
||||
if (!fs.existsSync(toolConfigPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const toolConfigRaw = await fs.promises.readFile(
|
||||
toolConfigPath,
|
||||
'utf-8'
|
||||
)
|
||||
const toolConfig = JSON.parse(
|
||||
toolConfigRaw
|
||||
) as ToolkitToolDefinition
|
||||
toolkitTools[toolId] = toolConfig
|
||||
} catch (e) {
|
||||
LogHelper.title('Toolkit Registry')
|
||||
LogHelper.error(
|
||||
`Failed to load tool config at "${toolConfigPath}": ${e}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
toolkits.push({
|
||||
id: toolkitId,
|
||||
name: toolkitConfig.name,
|
||||
description: toolkitConfig.description,
|
||||
iconName: toolkitConfig.icon_name,
|
||||
contextFiles,
|
||||
tools: toolkitTools
|
||||
})
|
||||
} catch (e) {
|
||||
LogHelper.title('Toolkit Registry')
|
||||
LogHelper.error(
|
||||
`Failed to load toolkit config at "${toolkitConfigPath}": ${e}`
|
||||
)
|
||||
}
|
||||
}
|
||||
const toolkits = [...toolkitsById.values()].filter(
|
||||
(toolkit) => toolkit.tools && Object.keys(toolkit.tools).length > 0
|
||||
)
|
||||
|
||||
this._toolkits = toolkits
|
||||
this._isLoaded = true
|
||||
@@ -361,7 +275,123 @@ export default class ToolkitRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeContextFilename(filename: string): string | null {
|
||||
private async loadBuiltInToolkits(
|
||||
toolkitsById: Map<string, ToolkitDefinition>
|
||||
): Promise<void> {
|
||||
if (!fs.existsSync(TOOLS_PATH)) {
|
||||
return
|
||||
}
|
||||
|
||||
const entries = await fs.promises.readdir(TOOLS_PATH, {
|
||||
withFileTypes: true
|
||||
})
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const toolkitId = entry.name
|
||||
const toolkitPath = path.join(TOOLS_PATH, toolkitId)
|
||||
const toolkitConfigPath = path.join(toolkitPath, 'toolkit.json')
|
||||
|
||||
if (!fs.existsSync(toolkitConfigPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const toolkitConfig = await this.loadToolkitConfig(toolkitConfigPath)
|
||||
const existingToolkit = toolkitsById.get(toolkitId)
|
||||
const toolkit: ToolkitDefinition = existingToolkit || {
|
||||
id: toolkitId,
|
||||
name: toolkitConfig.name,
|
||||
description: toolkitConfig.description,
|
||||
iconName: toolkitConfig.icon_name,
|
||||
contextFiles: this.normalizeContextFiles(
|
||||
toolkitConfig.context_files
|
||||
),
|
||||
tools: {}
|
||||
}
|
||||
|
||||
for (const toolId of toolkitConfig.tools || []) {
|
||||
if (ProfileHelper.isToolDisabled(toolId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
await this.loadToolConfig(
|
||||
toolkit,
|
||||
toolId,
|
||||
path.join(toolkitPath, toolId, 'tool.json')
|
||||
)
|
||||
}
|
||||
|
||||
toolkitsById.set(toolkitId, toolkit)
|
||||
} catch (e) {
|
||||
LogHelper.title('Toolkit Registry')
|
||||
LogHelper.error(
|
||||
`Failed to load toolkit config at "${toolkitConfigPath}": ${e}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async loadToolkitConfig(toolkitConfigPath: string): Promise<{
|
||||
name: string
|
||||
description: string
|
||||
icon_name: string
|
||||
context_files?: string[]
|
||||
tools?: string[]
|
||||
}> {
|
||||
return JSON.parse(
|
||||
await fs.promises.readFile(toolkitConfigPath, 'utf-8')
|
||||
) as {
|
||||
name: string
|
||||
description: string
|
||||
icon_name: string
|
||||
context_files?: string[]
|
||||
tools?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
private async loadToolConfig(
|
||||
toolkit: ToolkitDefinition,
|
||||
toolId: string,
|
||||
toolConfigPath: string
|
||||
): Promise<void> {
|
||||
if (!fs.existsSync(toolConfigPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const toolConfigRaw = await fs.promises.readFile(toolConfigPath, 'utf-8')
|
||||
const toolConfig = JSON.parse(toolConfigRaw) as ToolkitToolDefinition
|
||||
toolkit.tools = {
|
||||
...(toolkit.tools || {}),
|
||||
[toolId]: toolConfig
|
||||
}
|
||||
} catch (e) {
|
||||
LogHelper.title('Toolkit Registry')
|
||||
LogHelper.error(
|
||||
`Failed to load tool config at "${toolConfigPath}": ${e}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeContextFiles(contextFiles: unknown): string[] {
|
||||
return Array.isArray(contextFiles)
|
||||
? [
|
||||
...new Set(
|
||||
contextFiles
|
||||
.map((contextFile) => this.normalizeContextFilename(contextFile))
|
||||
.filter((contextFile): contextFile is string =>
|
||||
Boolean(contextFile)
|
||||
)
|
||||
)
|
||||
]
|
||||
: []
|
||||
}
|
||||
|
||||
private normalizeContextFilename(filename: unknown): string | null {
|
||||
if (typeof filename !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROFILE_DISABLED_PATH } from '@/constants'
|
||||
import { PROFILE_DOT_ENV_PATH } from '@/leon-roots'
|
||||
|
||||
interface ProfileDisabledConfig {
|
||||
skills?: string[]
|
||||
tools?: string[]
|
||||
}
|
||||
|
||||
const ENV_LINE_SEPARATOR_PATTERN = /\r?\n/
|
||||
const ENV_VARIABLE_NAME_PATTERN = /^[A-Z0-9_]+$/
|
||||
|
||||
function splitEnvLines(content: string): string[] {
|
||||
return content.split(ENV_LINE_SEPARATOR_PATTERN)
|
||||
}
|
||||
|
||||
function getEnvVariableName(line: string): string | null {
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
if (
|
||||
trimmedLine === '' ||
|
||||
trimmedLine.startsWith('#') ||
|
||||
!trimmedLine.includes('=')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const variableName = trimmedLine.slice(0, trimmedLine.indexOf('=')).trim()
|
||||
|
||||
return ENV_VARIABLE_NAME_PATTERN.test(variableName) ? variableName : null
|
||||
}
|
||||
|
||||
function readDisabledConfig(): ProfileDisabledConfig {
|
||||
if (!fs.existsSync(PROFILE_DISABLED_PATH)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(
|
||||
fs.readFileSync(PROFILE_DISABLED_PATH, 'utf8')
|
||||
) as ProfileDisabledConfig
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDisabledIds(ids: unknown): Set<string> {
|
||||
if (!Array.isArray(ids)) {
|
||||
return new Set()
|
||||
}
|
||||
|
||||
return new Set(
|
||||
ids
|
||||
.filter((id): id is string => typeof id === 'string')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id.length > 0)
|
||||
)
|
||||
}
|
||||
|
||||
export class ProfileHelper {
|
||||
/**
|
||||
* Get disabled skill ids from the active profile.
|
||||
*/
|
||||
public static getDisabledSkills(): Set<string> {
|
||||
return normalizeDisabledIds(readDisabledConfig().skills)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get disabled tool ids from the active profile.
|
||||
*/
|
||||
public static getDisabledTools(): Set<string> {
|
||||
return normalizeDisabledIds(readDisabledConfig().tools)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a skill is disabled in the active profile.
|
||||
* @param skillName The skill id
|
||||
*/
|
||||
public static isSkillDisabled(skillName: string): boolean {
|
||||
return this.getDisabledSkills().has(skillName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a tool is disabled in the active profile.
|
||||
* @param toolId The tool id
|
||||
*/
|
||||
public static isToolDisabled(toolId: string): boolean {
|
||||
return this.getDisabledTools().has(toolId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert a single variable inside the profile `.env`.
|
||||
* @param variableName The environment variable name
|
||||
* @param value The environment variable value
|
||||
*/
|
||||
public static async updateDotEnvVariable(
|
||||
variableName: string,
|
||||
value: string
|
||||
): Promise<void> {
|
||||
const dotEnvContent = fs.existsSync(PROFILE_DOT_ENV_PATH)
|
||||
? await fs.promises.readFile(PROFILE_DOT_ENV_PATH, 'utf8')
|
||||
: ''
|
||||
const dotEnvLines = dotEnvContent === '' ? [] : splitEnvLines(dotEnvContent)
|
||||
const nextLine = `${variableName}=${value}`
|
||||
let hasUpdatedLine = false
|
||||
|
||||
const updatedLines = dotEnvLines.map((line) => {
|
||||
if (getEnvVariableName(line) !== variableName) {
|
||||
return line
|
||||
}
|
||||
|
||||
hasUpdatedLine = true
|
||||
|
||||
return nextLine
|
||||
})
|
||||
|
||||
if (!hasUpdatedLine) {
|
||||
updatedLines.push(nextLine)
|
||||
}
|
||||
|
||||
const normalizedLines = updatedLines.filter(
|
||||
(line, index, lines) => !(index === lines.length - 1 && line === '')
|
||||
)
|
||||
|
||||
await fs.promises.mkdir(path.dirname(PROFILE_DOT_ENV_PATH), {
|
||||
recursive: true
|
||||
})
|
||||
|
||||
await fs.promises.writeFile(
|
||||
PROFILE_DOT_ENV_PATH,
|
||||
`${normalizedLines.join('\n')}\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { LEON_HOME_PATH, LEON_PROFILE_PATH } from '@/leon-roots'
|
||||
import { LEON_HOME_PATH } from '@/leon-roots'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
export class RuntimeHelper {
|
||||
@@ -208,33 +208,6 @@ export class RuntimeHelper {
|
||||
return this.firstExistingPath(venvCandidates) || this.getPythonBinPath()
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep skill-owned runtime artifacts out of `src` so install/update can clean
|
||||
* them up independently from skill source files.
|
||||
*/
|
||||
public static getSkillRuntimePath(skillPath: string): string {
|
||||
return path.join(
|
||||
LEON_PROFILE_PATH,
|
||||
'skills',
|
||||
path.basename(path.resolve(skillPath)),
|
||||
'.runtime'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the runtime node_modules directory for a Node.js skill.
|
||||
*/
|
||||
public static getNodejsSkillRuntimeNodeModulesPath(skillPath: string): string {
|
||||
return path.join(this.getSkillRuntimePath(skillPath), 'node_modules')
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the vendored Python dependency directory for a Python skill.
|
||||
*/
|
||||
public static getPythonSkillRuntimeVendorPath(skillPath: string): string {
|
||||
return path.join(this.getSkillRuntimePath(skillPath), 'vendor')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a shell-safe command string for execa/spawn call sites that currently
|
||||
* rely on `shell: true`.
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SKILLS_PATH
|
||||
} from '@/constants'
|
||||
import { FileHelper } from '@/helpers/file-helper'
|
||||
import { ProfileHelper } from '@/helpers/profile-helper'
|
||||
|
||||
interface SkillDomain {
|
||||
domainId: string
|
||||
@@ -68,10 +69,24 @@ export class SkillDomainHelper {
|
||||
* List all skill folders
|
||||
*/
|
||||
public static listSkillFoldersSync(): string[] {
|
||||
return fs
|
||||
.readdirSync(SKILLS_PATH)
|
||||
.filter((folder) => folder.endsWith(SKILL_NAME_SUFFIX))
|
||||
.sort()
|
||||
const skillFolders = new Set<string>()
|
||||
|
||||
for (const skillsPath of [SKILLS_PATH, PROFILE_SKILLS_PATH]) {
|
||||
if (!fs.existsSync(skillsPath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const folder of fs.readdirSync(skillsPath)) {
|
||||
if (
|
||||
folder.endsWith(SKILL_NAME_SUFFIX) &&
|
||||
!ProfileHelper.isSkillDisabled(folder)
|
||||
) {
|
||||
skillFolders.add(folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...skillFolders].sort()
|
||||
}
|
||||
|
||||
public static async listSkillFolders(): Promise<string[]> {
|
||||
@@ -119,7 +134,12 @@ export class SkillDomainHelper {
|
||||
public static getNewSkillConfigPath(
|
||||
skillName: SkillSchema['name']
|
||||
): string | null {
|
||||
const skillPath = path.join(SKILLS_PATH, skillName)
|
||||
const skillPath = this.resolveSkillPath(skillName)
|
||||
|
||||
if (!skillPath) {
|
||||
return null
|
||||
}
|
||||
|
||||
const skillConfigPath = path.join(skillPath, 'skill.json')
|
||||
|
||||
if (!fs.existsSync(skillConfigPath)) {
|
||||
@@ -129,6 +149,28 @@ export class SkillDomainHelper {
|
||||
return skillConfigPath
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a skill source path for the active profile.
|
||||
* Profile-installed skills override built-in skills with the same ID.
|
||||
* @param skillName Skill name to resolve
|
||||
*/
|
||||
public static resolveSkillPath(skillName: SkillSchema['name']): string | null {
|
||||
if (ProfileHelper.isSkillDisabled(skillName)) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const skillsPath of [PROFILE_SKILLS_PATH, SKILLS_PATH]) {
|
||||
const skillPath = path.join(skillsPath, skillName)
|
||||
const skillConfigPath = path.join(skillPath, 'skill.json')
|
||||
|
||||
if (fs.existsSync(skillConfigPath)) {
|
||||
return skillPath
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get skill guidance path (SKILL.md)
|
||||
* @param skillName Skill name to get guidance path from
|
||||
@@ -136,7 +178,12 @@ export class SkillDomainHelper {
|
||||
public static getSkillGuidancePath(
|
||||
skillName: SkillSchema['name']
|
||||
): string | null {
|
||||
const skillPath = path.join(SKILLS_PATH, skillName)
|
||||
const skillPath = this.resolveSkillPath(skillName)
|
||||
|
||||
if (!skillPath) {
|
||||
return null
|
||||
}
|
||||
|
||||
const skillGuidancePath = path.join(skillPath, 'SKILL.md')
|
||||
|
||||
if (!fs.existsSync(skillGuidancePath)) {
|
||||
@@ -439,9 +486,14 @@ export class SkillDomainHelper {
|
||||
lang: ShortLanguageCode,
|
||||
skillName: SkillSchema['name']
|
||||
): Promise<SkillLocaleConfigSchema | object> {
|
||||
const skillPath = this.resolveSkillPath(skillName)
|
||||
|
||||
if (!skillPath) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const skillLocaleConfigPath = path.join(
|
||||
SKILLS_PATH,
|
||||
skillName,
|
||||
skillPath,
|
||||
'locales',
|
||||
`${lang}.json`
|
||||
)
|
||||
|
||||
@@ -26,8 +26,7 @@ import { SkillDomainHelper } from '@/helpers/skill-domain-helper'
|
||||
import {
|
||||
MINIMUM_REQUIRED_RAM,
|
||||
VOICE_CONFIG_PATH,
|
||||
GLOBAL_DATA_PATH,
|
||||
SKILLS_PATH
|
||||
GLOBAL_DATA_PATH
|
||||
} from '@/constants'
|
||||
import { SystemHelper } from '@/helpers/system-helper'
|
||||
|
||||
@@ -161,7 +160,12 @@ const GLOBAL_DATA_SCHEMAS = {
|
||||
const skillNames = await SkillDomainHelper.listSkillFolders()
|
||||
|
||||
for (const skillName of skillNames) {
|
||||
const skillPath = path.join(SKILLS_PATH, skillName)
|
||||
const skillPath = SkillDomainHelper.resolveSkillPath(skillName)
|
||||
|
||||
if (!skillPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
const pathToSkill = path.join(skillPath, 'skill.json')
|
||||
const skillObject: SkillSchema = JSON.parse(
|
||||
await fs.promises.readFile(pathToSkill, 'utf8')
|
||||
|
||||
@@ -10,7 +10,7 @@ export const run: ActionFunction = async function (params) {
|
||||
const answer = answers[Math.floor(Math.random() * answers.length)]
|
||||
|
||||
if (answer === 'magical_day') {
|
||||
return leon.answer({
|
||||
await leon.answer({
|
||||
key: 'magical_day',
|
||||
data: {
|
||||
weekday: LEON_BIRTH_DATE.toLocaleString(params.lang, {
|
||||
@@ -21,10 +21,11 @@ export const run: ActionFunction = async function (params) {
|
||||
year: LEON_BIRTH_DATE.getFullYear()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (answer === 'commemorate') {
|
||||
return leon.answer({
|
||||
await leon.answer({
|
||||
key: 'commemorate',
|
||||
data: {
|
||||
month: LEON_BIRTH_DATE.toLocaleString(params.lang, { month: 'long' }),
|
||||
@@ -32,6 +33,7 @@ export const run: ActionFunction = async function (params) {
|
||||
year: LEON_BIRTH_DATE.getFullYear()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const currentDate = new Date()
|
||||
|
||||
@@ -11,17 +11,19 @@ export const run: ActionFunction = async function (params) {
|
||||
: null
|
||||
|
||||
if (typeof timeZone !== 'string') {
|
||||
return await leon.answer({
|
||||
await leon.answer({
|
||||
key: 'time_zone_not_found'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
Intl.DateTimeFormat('en', { timeZone })
|
||||
} catch {
|
||||
return await leon.answer({
|
||||
await leon.answer({
|
||||
key: 'time_zone_not_found'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const currentDate = new Date(new Date().toLocaleString('en', { timeZone }))
|
||||
|
||||
@@ -16,17 +16,19 @@ const daysBetween = (date1: Date, date2: Date): number => {
|
||||
export const run: ActionFunction = async function (params) {
|
||||
const targetDateValue = params.action_arguments['target_date']
|
||||
if (typeof targetDateValue !== 'string') {
|
||||
return await leon.answer({
|
||||
await leon.answer({
|
||||
key: 'days_countdown_error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const currentDate = new Date()
|
||||
const futureDate = new Date(targetDateValue)
|
||||
if (Number.isNaN(futureDate.getTime())) {
|
||||
return await leon.answer({
|
||||
await leon.answer({
|
||||
key: 'days_countdown_error'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const daysCountdown = daysBetween(currentDate, futureDate)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from bridges.python.src.sdk.leon import leon
|
||||
from bridges.python.src.sdk.types import ActionParams
|
||||
from bridges.python.src.sdk.tools.inference import InferenceTool
|
||||
from tools.communication.inference import InferenceTool
|
||||
from ..lib import memory
|
||||
|
||||
groups = [
|
||||
|
||||
@@ -6,7 +6,7 @@ import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import ElevenLabsAudioTool from '@sdk/tools/elevenlabs_audio'
|
||||
import ElevenLabsAudioTool from '@tools/music_audio/elevenlabs_audio'
|
||||
import {
|
||||
formatBytes,
|
||||
formatFilePath,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import UltimateVocalRemoverONNXTool from '@sdk/tools/ultimate_vocal_remover_onnx'
|
||||
import UltimateVocalRemoverONNXTool from '@tools/music_audio/ultimate_vocal_remover_onnx'
|
||||
import { formatFilePath } from '@sdk/utils'
|
||||
|
||||
export const run: ActionFunction = async function (
|
||||
|
||||
@@ -6,11 +6,11 @@ import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import FasterWhisperTool from '@sdk/tools/faster_whisper'
|
||||
import Qwen3ASRTool from '@sdk/tools/qwen3_asr'
|
||||
import OpenAIAudioTool from '@sdk/tools/openai_audio'
|
||||
import AssemblyAIAudioTool from '@sdk/tools/assemblyai_audio'
|
||||
import ElevenLabsAudioTool from '@sdk/tools/elevenlabs_audio'
|
||||
import FasterWhisperTool from '@tools/music_audio/faster_whisper'
|
||||
import Qwen3ASRTool from '@tools/music_audio/qwen3_asr'
|
||||
import OpenAIAudioTool from '@tools/music_audio/openai_audio'
|
||||
import AssemblyAIAudioTool from '@tools/music_audio/assemblyai_audio'
|
||||
import ElevenLabsAudioTool from '@tools/music_audio/elevenlabs_audio'
|
||||
import { formatFilePath } from '@sdk/utils'
|
||||
|
||||
interface MusicAudioToolkitSkillSettings extends Record<string, unknown> {
|
||||
|
||||
@@ -3,11 +3,11 @@ import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import GrokTool from '@sdk/tools/grok'
|
||||
import OpenRouterTool from '@sdk/tools/openrouter'
|
||||
import ChatterboxONNXTool from '@sdk/tools/chatterbox_onnx'
|
||||
import FfmpegTool from '@sdk/tools/ffmpeg'
|
||||
import FfprobeTool from '@sdk/tools/ffprobe'
|
||||
import GrokTool from '@tools/search_web/grok'
|
||||
import OpenRouterTool from '@tools/communication/openrouter'
|
||||
import ChatterboxONNXTool from '@tools/music_audio/chatterbox_onnx'
|
||||
import FfmpegTool from '@tools/video_streaming/ffmpeg'
|
||||
import FfprobeTool from '@tools/video_streaming/ffprobe'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs/promises'
|
||||
import os from 'node:os'
|
||||
@@ -233,7 +233,7 @@ Generate the script as a JSON object with this structure:
|
||||
|
||||
// Calculate total duration by measuring each segment
|
||||
let totalDurationMs = 0
|
||||
const segmentsWithTiming: Array<{ path: string; startMs: number }> = []
|
||||
const segmentsWithTiming: Array<{ path: string, startMs: number }> = []
|
||||
|
||||
for (const segmentPath of segmentPaths) {
|
||||
const duration = await ffprobe.getDuration(segmentPath)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import GrokTool from '@sdk/tools/grok'
|
||||
import GrokTool from '@tools/search_web/grok'
|
||||
|
||||
interface SearchSkillSettings extends Record<string, unknown> {
|
||||
search_provider?: string
|
||||
|
||||
@@ -6,7 +6,7 @@ import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import OpenCodeTool from '@sdk/tools/opencode'
|
||||
import OpenCodeTool from '@tools/coding_development/opencode'
|
||||
import { buildSkillPrompt, getContextFiles } from '../lib/skill-prompt'
|
||||
|
||||
interface SkillWriterSettings extends Record<string, unknown> {
|
||||
|
||||
@@ -3,20 +3,16 @@ import path from 'node:path'
|
||||
|
||||
import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import OpenCodeTool from '@sdk/tools/opencode'
|
||||
import OpenCodeTool from '@tools/coding_development/opencode'
|
||||
import { buildSkillPrompt, getContextFiles } from '../lib/skill-prompt'
|
||||
|
||||
interface SkillWriterSettings extends Record<string, unknown> {
|
||||
opencode_openrouter_model?: string
|
||||
}
|
||||
|
||||
export const run: ActionFunction = async function (
|
||||
_params: ActionParams,
|
||||
_paramsHelper: ParamsHelper
|
||||
) {
|
||||
export const run: ActionFunction = async function (_params: ActionParams) {
|
||||
try {
|
||||
const description = _params.utterance
|
||||
const targetPath = process.cwd()
|
||||
|
||||
@@ -475,7 +475,7 @@ def run(params: ActionParams, params_helper: ParamsHelper) -> None:
|
||||
\`\`\`
|
||||
|
||||
## Available SDK Tools
|
||||
When generating code, you can use these existing tools (import from '@sdk/tools/TOOL_NAME'):
|
||||
When generating code, you can use these existing tools (import from '@tools/<toolkit_name>/<tool_name>'):
|
||||
- **cerebras-tool**: Cerebras LLM API (chat, completion, structured output, list models)
|
||||
- **openrouter-tool**: OpenRouter LLM API (chat, completion, list models)
|
||||
- **ytdlp-tool**: Download videos from YouTube and other platforms
|
||||
|
||||
@@ -11,7 +11,8 @@ export const run: ActionFunction = async function (_params, paramsHelper) {
|
||||
: await getNewestTimerMemory()
|
||||
|
||||
if (!timerMemory) {
|
||||
return await leon.answer({ key: 'no_timer_set' })
|
||||
await leon.answer({ key: 'no_timer_set' })
|
||||
return
|
||||
}
|
||||
|
||||
const { interval, finishedAt, duration } = timerMemory
|
||||
|
||||
@@ -18,12 +18,14 @@ export const run: ActionFunction = async function (params) {
|
||||
typeof duration.value !== 'number' ||
|
||||
typeof duration.unit !== 'string'
|
||||
) {
|
||||
return leon.answer({ key: 'cannot_get_duration' })
|
||||
await leon.answer({ key: 'cannot_get_duration' })
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedUnit = duration.unit.toLowerCase()
|
||||
if (!supportedUnits.includes(normalizedUnit)) {
|
||||
return leon.answer({ key: 'unit_not_supported' })
|
||||
await leon.answer({ key: 'unit_not_supported' })
|
||||
return
|
||||
}
|
||||
|
||||
const { value: durationValue } = duration
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"@@/*": ["../*"],
|
||||
"@/*": ["../server/src/*"],
|
||||
"@bridge/*": ["../bridges/nodejs/src/*"],
|
||||
"@sdk/*": ["../bridges/nodejs/src/sdk/*"]
|
||||
"@sdk/*": ["../bridges/nodejs/src/sdk/*"],
|
||||
"@tools/*": ["../tools/*/src/nodejs/index.ts", "../tools/*.ts"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import FfmpegTool from '@sdk/tools/ffmpeg'
|
||||
import YtdlpTool from '@sdk/tools/ytdlp'
|
||||
import FfmpegTool from '@tools/video_streaming/ffmpeg'
|
||||
import YtdlpTool from '@tools/video_streaming/ytdlp'
|
||||
import { formatFilePath } from '@sdk/utils'
|
||||
|
||||
const isHttpUrl = (value: string): boolean => {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import OpenRouterTool from '@sdk/tools/openrouter'
|
||||
import type { TranscriptionOutput } from '@sdk/tools/transcription-schema'
|
||||
import OpenRouterTool from '@tools/communication/openrouter'
|
||||
import type { TranscriptionOutput } from '@tools/music_audio/transcription-schema'
|
||||
|
||||
interface VideoSummarizerSettings extends Record<string, unknown> {
|
||||
openrouter_model?: string | null
|
||||
@@ -30,7 +30,7 @@ const buildTranscriptText = (
|
||||
const truncateTranscript = (
|
||||
transcript: string,
|
||||
maxChars: number
|
||||
): { text: string; truncated: boolean } => {
|
||||
): { text: string, truncated: boolean } => {
|
||||
if (transcript.length <= maxChars) {
|
||||
return { text: transcript, truncated: false }
|
||||
}
|
||||
|
||||
@@ -2,15 +2,17 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import type { TranscriptionOutput } from '@sdk/tools/transcription-schema'
|
||||
import type { TranscriptionOutput } from '@tools/music_audio/transcription-schema'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import ChatterboxONNXTool from '@sdk/tools/chatterbox_onnx'
|
||||
import Qwen3TTSTool from '@sdk/tools/qwen3_tts'
|
||||
import FfmpegTool from '@sdk/tools/ffmpeg'
|
||||
import FfprobeTool from '@sdk/tools/ffprobe'
|
||||
import ChatterboxONNXTool from '@tools/music_audio/chatterbox_onnx'
|
||||
import Qwen3TTSTool, {
|
||||
type SupportedLanguage
|
||||
} from '@tools/music_audio/qwen3_tts'
|
||||
import FfmpegTool from '@tools/video_streaming/ffmpeg'
|
||||
import FfprobeTool from '@tools/video_streaming/ffprobe'
|
||||
import { formatFilePath, normalizeLanguageCode } from '@sdk/utils'
|
||||
|
||||
interface SpeakerReference {
|
||||
@@ -68,13 +70,13 @@ function toMs(seconds: number): number {
|
||||
function splitSegmentText(
|
||||
segment: Segment,
|
||||
maxChars: number
|
||||
): Array<{ text: string; ratio: number }> {
|
||||
): Array<{ text: string, ratio: number }> {
|
||||
const text = segment.text.trim()
|
||||
if (text.length <= maxChars) {
|
||||
return [{ text, ratio: 1.0 }]
|
||||
}
|
||||
|
||||
const chunks: Array<{ text: string; ratio: number }> = []
|
||||
const chunks: Array<{ text: string, ratio: number }> = []
|
||||
let remaining = text
|
||||
const totalLength = text.length
|
||||
|
||||
@@ -314,6 +316,9 @@ export const run: ActionFunction = async function (
|
||||
return
|
||||
}
|
||||
|
||||
const resolvedTargetLanguageLabel =
|
||||
targetLanguageLabel || getLanguageDisplayName(targetLanguage)
|
||||
|
||||
// Read and parse transcription
|
||||
const transcriptionContent = await fs.promises.readFile(
|
||||
translatedTranscriptionPath,
|
||||
@@ -333,7 +338,7 @@ export const run: ActionFunction = async function (
|
||||
data: {
|
||||
segment_count: transcription.segments.length.toString(),
|
||||
speaker_count: speakerReferences.length.toString(),
|
||||
target_language: targetLanguageLabel,
|
||||
target_language: resolvedTargetLanguageLabel,
|
||||
provider
|
||||
}
|
||||
})
|
||||
@@ -465,7 +470,7 @@ export const run: ActionFunction = async function (
|
||||
const qwen3TTSTool = await ToolManager.initTool(Qwen3TTSTool)
|
||||
const qwenTasks = synthesisTasks.map((task) => ({
|
||||
text: task.text,
|
||||
target_language: targetLanguageLabel ?? 'Auto',
|
||||
target_language: resolvedTargetLanguageLabel as SupportedLanguage,
|
||||
audio_path: task.audio_path,
|
||||
speaker_reference_path: task.speaker_reference_path,
|
||||
x_vector_only_mode: true
|
||||
@@ -699,14 +704,14 @@ export const run: ActionFunction = async function (
|
||||
output_path: formatFilePath(finalAudioPath),
|
||||
output_folder: formatFilePath(processedSegmentsDir),
|
||||
manifest_path: formatFilePath(manifestPath),
|
||||
target_language: targetLanguageLabel
|
||||
target_language: resolvedTargetLanguageLabel
|
||||
},
|
||||
core: {
|
||||
context_data: {
|
||||
processed_segments_dir: processedSegmentsDir,
|
||||
segments_manifest_path: manifestPath,
|
||||
dubbed_audio_path: finalAudioPath,
|
||||
target_language: targetLanguageLabel,
|
||||
target_language: resolvedTargetLanguageLabel,
|
||||
target_language_code: targetLanguage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import ECAPATool from '@sdk/tools/ecapa'
|
||||
import ECAPATool from '@tools/music_audio/ecapa'
|
||||
|
||||
interface SpeakerReference {
|
||||
speaker: string
|
||||
@@ -37,7 +37,7 @@ export const run: ActionFunction = async function (
|
||||
}
|
||||
|
||||
const tool = await ToolManager.initTool(ECAPATool)
|
||||
const results: { speaker: string; gender: string }[] = []
|
||||
const results: { speaker: string, gender: string }[] = []
|
||||
|
||||
for (const ref of speakerReferences) {
|
||||
const clips = [ref.reference1_path, ref.reference2_path]
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import YtdlpTool from '@sdk/tools/ytdlp'
|
||||
import YtdlpTool from '@tools/video_streaming/ytdlp'
|
||||
import { formatFilePath, normalizeLanguageCode } from '@sdk/utils'
|
||||
|
||||
import { DownloadProgressWidget } from '../widgets/download-progress-widget'
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import FfmpegTool from '@sdk/tools/ffmpeg'
|
||||
import FfmpegTool from '@tools/video_streaming/ffmpeg'
|
||||
import { formatBytes, formatFilePath } from '@sdk/utils'
|
||||
|
||||
export const run: ActionFunction = async function (
|
||||
|
||||
@@ -2,11 +2,11 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import type { TranscriptionOutput } from '@sdk/tools/transcription-schema'
|
||||
import type { TranscriptionOutput } from '@tools/music_audio/transcription-schema'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import FfmpegTool from '@sdk/tools/ffmpeg'
|
||||
import FfmpegTool from '@tools/video_streaming/ffmpeg'
|
||||
import { formatFilePath } from '@sdk/utils'
|
||||
|
||||
interface SpeakerReference {
|
||||
@@ -259,10 +259,10 @@ export const run: ActionFunction = async function (
|
||||
* Combines consecutive segments if needed to reach required duration
|
||||
*/
|
||||
function findBestSegment(
|
||||
segments: Array<{ from: number; to: number }>,
|
||||
segments: Array<{ from: number, to: number }>,
|
||||
requiredDuration: number,
|
||||
excludeSegment: { start: number; end: number } | null
|
||||
): { start: number; end: number } | null {
|
||||
excludeSegment: { start: number, end: number } | null
|
||||
): { start: number, end: number } | null {
|
||||
// Segments are already sorted by time (from), find the earliest usable one
|
||||
for (let i = 0; i < segments.length; i += 1) {
|
||||
const currentSegment = segments[i]
|
||||
@@ -323,13 +323,18 @@ function findBestSegment(
|
||||
* Find the longest single segment from the available segments
|
||||
*/
|
||||
function findLongestSegment(
|
||||
segments: Array<{ from: number; to: number }>
|
||||
): { start: number; end: number } | null {
|
||||
segments: Array<{ from: number, to: number }>
|
||||
): { start: number, end: number } | null {
|
||||
if (segments.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
let longestSegment = segments[0]
|
||||
const firstSegment = segments[0]
|
||||
if (!firstSegment) {
|
||||
return null
|
||||
}
|
||||
|
||||
let longestSegment = firstSegment
|
||||
let maxDuration = longestSegment.to - longestSegment.from
|
||||
|
||||
for (let i = 1; i < segments.length; i += 1) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import FfmpegTool from '@sdk/tools/ffmpeg'
|
||||
import FfmpegTool from '@tools/video_streaming/ffmpeg'
|
||||
import {
|
||||
formatBytes,
|
||||
formatFilePath,
|
||||
|
||||
@@ -2,12 +2,12 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import type { ActionFunction, ActionParams } from '@sdk/types'
|
||||
import type { TranscriptionOutput } from '@sdk/tools/transcription-schema'
|
||||
import type { TranscriptionOutput } from '@tools/music_audio/transcription-schema'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import { Settings } from '@sdk/settings'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import OpenRouterTool from '@sdk/tools/openrouter'
|
||||
import OpenRouterTool from '@tools/communication/openrouter'
|
||||
import { formatFilePath } from '@sdk/utils'
|
||||
|
||||
interface VideoTranslatorSkillSettings extends Record<string, unknown> {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ActionFunction } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import Qwen3TtsTool from '@sdk/tools/qwen3_tts'
|
||||
import Qwen3TtsTool from '@tools/music_audio/qwen3_tts'
|
||||
import { formatFilePath } from '@sdk/utils'
|
||||
|
||||
function sanitizeFileName(value: string): string {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ActionFunction } from '@sdk/types'
|
||||
import { leon } from '@sdk/leon'
|
||||
import { ParamsHelper } from '@sdk/params-helper'
|
||||
import ToolManager, { isMissingToolSettingsError } from '@sdk/tool-manager'
|
||||
import OpenMeteoTool from '@sdk/tools/open-meteo'
|
||||
import OpenMeteoTool from '@tools/weather/openmeteo'
|
||||
import { WeatherForecastWidget } from '../widgets/weather-forecast-widget'
|
||||
|
||||
type Units = 'metric' | 'imperial'
|
||||
|
||||
@@ -183,7 +183,7 @@ export class WeatherForecastWidget extends Widget<Params> {
|
||||
|
||||
private getWeatherIcon(description: string): string {
|
||||
const desc = description.toLowerCase()
|
||||
const iconMap: Array<{ keywords: string[]; icon: string }> = [
|
||||
const iconMap: Array<{ keywords: string[], icon: string }> = [
|
||||
{ keywords: ['clear', 'sunny'], icon: 'sun' },
|
||||
{ keywords: ['cloud', 'overcast'], icon: 'cloud' },
|
||||
{ keywords: ['drizzle'], icon: 'drizzle' },
|
||||
|
||||
@@ -53,7 +53,7 @@ export class PlaygroundTestWidget extends Widget<Params> {
|
||||
children: 'Shopping List'
|
||||
}),
|
||||
new Form({
|
||||
onSubmit: (data): unknown => {
|
||||
onSubmit: (data: Record<string, unknown>): unknown => {
|
||||
return this.runSkillAction('submit_shopping_list', data)
|
||||
},
|
||||
children: [
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Business & Finance",
|
||||
"description": "Tools for business and finance.",
|
||||
"icon_name": "money-dollar-circle-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Calendar & Scheduling",
|
||||
"description": "Tools for calendars and scheduling.",
|
||||
"icon_name": "calendar-2-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from .src.python.opencode_tool import OpenCodeTool
|
||||
|
||||
__all__ = ["OpenCodeTool"]
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './opencode-tool'
|
||||
@@ -0,0 +1,68 @@
|
||||
{{SYSTEM_PROMPT_SECTION}}
|
||||
{{REPO_SNAPSHOT}}
|
||||
{{TOOLKIT_INFO}}
|
||||
|
||||
# Leon Skill Creation (Concise)
|
||||
|
||||
You are generating a Leon skill in **{{LANGUAGE}}**.
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Use the **{{BRIDGE}}** bridge for all source files.
|
||||
- Skills live directly under `skills/` (no subfolders).
|
||||
- All source files use `{{FILE_EXTENSION}}`.
|
||||
- Validate JSON files against `schemas/skill-schemas/*`.
|
||||
- Write all required files to disk under the chosen `skills/<name>_skill` folder.
|
||||
|
||||
## Required Structure
|
||||
|
||||
```
|
||||
skills/skill_name/
|
||||
skill.json
|
||||
locales/en.json
|
||||
src/
|
||||
settings.sample.json
|
||||
settings.json
|
||||
actions/
|
||||
widgets/ (optional)
|
||||
```
|
||||
|
||||
## skill.json Rules
|
||||
|
||||
- `actions` required, `flow` optional.
|
||||
- If `flow` exists, only the first action receives user parameters.
|
||||
- Use `"skill_name:action_name"` for cross-skill flow steps.
|
||||
- Set `author.name` to `Leon` unless explicitly specified.
|
||||
|
||||
## Settings Files
|
||||
|
||||
- `src/settings.sample.json` and `src/settings.json` must both exist and start identical.
|
||||
- Use `{}` if no settings.
|
||||
|
||||
## Toolkits (Plan First)
|
||||
|
||||
- Choose relevant toolkits from above **before** writing code.
|
||||
- Use existing tools instead of duplicating functionality.
|
||||
|
||||
## leon.answer Basics
|
||||
|
||||
{{LEON_ANSWER_BASIC_EXAMPLE}}
|
||||
|
||||
## Passing Data Between Actions
|
||||
|
||||
{{CONTEXT_DATA_EXAMPLE}}
|
||||
|
||||
## Settings Usage
|
||||
|
||||
{{SETTINGS_USAGE_EXAMPLE}}
|
||||
|
||||
## Widget Rules
|
||||
|
||||
- Do not use `Card` as the parent component. The `WidgetWrapper` is already applied by default.
|
||||
- For icons, use only the icon name without the `ri-` prefix and `-line` suffix. The system automatically completes them to `ri-{icon-name}-line`. For example, use `snow` instead of `ri-snow-line`.
|
||||
|
||||
## Action Parameters
|
||||
|
||||
{{ACTION_PARAMS_EXAMPLE}}
|
||||
|
||||
{{REFERENCE_FILES_SECTION}}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
{{SYSTEM_PROMPT_SECTION}}
|
||||
{{REPO_SNAPSHOT}}
|
||||
{{TOOLKIT_INFO}}
|
||||
|
||||
# Leon Skill Creation (Concise)
|
||||
|
||||
You are generating a Leon skill in **{{LANGUAGE}}**.
|
||||
|
||||
## Core Rules
|
||||
|
||||
- Use the **{{BRIDGE}}** bridge for all source files.
|
||||
- Skills live directly under `skills/` (no subfolders).
|
||||
- All source files use `{{FILE_EXTENSION}}`.
|
||||
- Validate JSON files against `schemas/skill-schemas/*`.
|
||||
- Write all required files to disk under the chosen `skills/<name>_skill` folder.
|
||||
|
||||
## Required Structure
|
||||
|
||||
```
|
||||
skills/skill_name/
|
||||
skill.json
|
||||
locales/en.json
|
||||
src/
|
||||
settings.sample.json
|
||||
settings.json
|
||||
actions/
|
||||
widgets/ (optional)
|
||||
```
|
||||
|
||||
## skill.json Rules
|
||||
|
||||
- `actions` required, `flow` optional.
|
||||
- If `flow` exists, only the first action receives user parameters.
|
||||
- Use `"skill_name:action_name"` for cross-skill flow steps.
|
||||
- Set `author.name` to `Leon` unless explicitly specified.
|
||||
|
||||
## Settings Files
|
||||
|
||||
- `src/settings.sample.json` and `src/settings.json` must both exist and start identical.
|
||||
- Use `{}` if no settings.
|
||||
|
||||
## Toolkits (Plan First)
|
||||
|
||||
- Choose relevant toolkits from above **before** writing code.
|
||||
- Use existing tools instead of duplicating functionality.
|
||||
|
||||
## leon.answer Basics
|
||||
|
||||
{{LEON_ANSWER_BASIC_EXAMPLE}}
|
||||
|
||||
## Passing Data Between Actions
|
||||
|
||||
{{CONTEXT_DATA_EXAMPLE}}
|
||||
|
||||
## Settings Usage
|
||||
|
||||
{{SETTINGS_USAGE_EXAMPLE}}
|
||||
|
||||
## Widget Rules
|
||||
|
||||
- Do not use `Card` as the parent component. The `WidgetWrapper` is already applied by default.
|
||||
- For icons, use only the icon name without the `ri-` prefix and `-line` suffix. The system automatically completes them to `ri-{icon-name}-line`. For example, use `snow` instead of `ri-snow-line`.
|
||||
|
||||
## Action Parameters
|
||||
|
||||
{{ACTION_PARAMS_EXAMPLE}}
|
||||
|
||||
{{REFERENCE_FILES_SECTION}}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"$schema": "../../../schemas/tool-schemas/tool.json",
|
||||
"tool_id": "opencode",
|
||||
"toolkit_id": "coding_development",
|
||||
"name": "OpenCode",
|
||||
"description": "An AI-powered coding agent tool that generates skills using multiple LLM providers (Cerebras, MiniMax, Anthropic, OpenAI, Gemini).",
|
||||
"icon_name": "code-box-line",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"binaries": {
|
||||
"linux-x86_64": "https://github.com/leon-ai/leon-binaries/releases/download/opencode-v1.14.29/opencode_1.14.29-linux-x86_64.tar.gz",
|
||||
"linux-aarch64": "https://github.com/leon-ai/leon-binaries/releases/download/opencode-v1.14.29/opencode_1.14.29-linux-aarch64.tar.gz",
|
||||
"macosx-x86_64": "https://github.com/leon-ai/leon-binaries/releases/download/opencode-v1.14.29/opencode_1.14.29-macosx-x86_64.zip",
|
||||
"macosx-arm64": "https://github.com/leon-ai/leon-binaries/releases/download/opencode-v1.14.29/opencode_1.14.29-macosx-arm64.zip",
|
||||
"win-amd64": "https://github.com/leon-ai/leon-binaries/releases/download/opencode-v1.14.29/opencode_1.14.29-win-amd64.zip"
|
||||
},
|
||||
"functions": {
|
||||
"configureProvider": {
|
||||
"description": "Configure a provider with an API key and optional model.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider",
|
||||
"apiKey"
|
||||
]
|
||||
}
|
||||
},
|
||||
"getConfiguredProviders": {
|
||||
"description": "List the providers currently configured with API keys.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"getAvailableProviders": {
|
||||
"description": "List providers supported by OpenCode.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
"getDefaultModel": {
|
||||
"description": "Get the default model name for a provider.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"provider": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"provider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"generateSkill": {
|
||||
"description": "Generate a new skill using OpenCode CLI with an agentic loop.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"api_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"target_path": {
|
||||
"type": "string"
|
||||
},
|
||||
"context_files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"bridge": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"provider",
|
||||
"target_path"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Coding & Development",
|
||||
"description": "Tools for code generation, development, and automation.",
|
||||
"icon_name": "code-s-slash-line",
|
||||
"context_files": [
|
||||
"ARCHITECTURE.md",
|
||||
"WORKSPACE_INTELLIGENCE.md",
|
||||
"LEON_RUNTIME.md",
|
||||
"HOME.md"
|
||||
],
|
||||
"tools": [
|
||||
"opencode"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from .src.python.cerebras_tool import CerebrasTool
|
||||
|
||||
__all__ = ["CerebrasTool"]
|
||||
@@ -0,0 +1,352 @@
|
||||
import { Tool } from '@sdk/base-tool'
|
||||
import { ToolkitConfig } from '@sdk/toolkit-config'
|
||||
import { Network, NetworkError } from '@sdk/network'
|
||||
|
||||
// Hardcoded default settings for Cerebras tool
|
||||
const CEREBRAS_API_KEY: string | null = null
|
||||
const CEREBRAS_MODEL = 'zai-glm-4.7'
|
||||
const DEFAULT_SETTINGS: Record<string, unknown> = {
|
||||
CEREBRAS_API_KEY,
|
||||
CEREBRAS_MODEL
|
||||
}
|
||||
const REQUIRED_SETTINGS = ['CEREBRAS_API_KEY']
|
||||
|
||||
interface ChatMessage {
|
||||
role: string
|
||||
content: string
|
||||
}
|
||||
|
||||
interface ChatCompletionOptions {
|
||||
messages: ChatMessage[]
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
use_structured_output?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema?: Record<string, any>
|
||||
}
|
||||
|
||||
interface CompletionOptions {
|
||||
prompt: string
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
use_structured_output?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema?: Record<string, any>
|
||||
}
|
||||
|
||||
interface StructuredCompletionOptions {
|
||||
prompt: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema: Record<string, any>
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: any
|
||||
model_used?: string
|
||||
error?: string
|
||||
status_code?: number
|
||||
}
|
||||
|
||||
export default class CerebrasTool extends Tool {
|
||||
private static readonly TOOLKIT = 'communication'
|
||||
private readonly config: ReturnType<typeof ToolkitConfig.load>
|
||||
private api_key: string | null
|
||||
private model: string
|
||||
private readonly network: Network
|
||||
|
||||
// Popular Cerebras-hosted models (override with full model IDs if needed)
|
||||
private readonly popular_models = {
|
||||
'zai-glm-4.7': 'zai-glm-4.7',
|
||||
'qwen-3-235b-a22b-instruct-2507': 'qwen-3-235b-a22b-instruct-2507',
|
||||
'qwen-3-32b': 'qwen-3-32b'
|
||||
}
|
||||
|
||||
constructor(apiKey?: string) {
|
||||
super()
|
||||
// Load configuration from central toolkits directory
|
||||
this.config = ToolkitConfig.load(CerebrasTool.TOOLKIT, this.toolName)
|
||||
|
||||
const toolSettings = ToolkitConfig.loadToolSettings(
|
||||
CerebrasTool.TOOLKIT,
|
||||
this.toolName,
|
||||
DEFAULT_SETTINGS
|
||||
)
|
||||
this.settings = toolSettings
|
||||
this.requiredSettings = REQUIRED_SETTINGS
|
||||
this.checkRequiredSettings(this.toolName)
|
||||
|
||||
// Priority: skill-provided apiKey > toolkit settings > hardcoded default
|
||||
this.api_key =
|
||||
apiKey ||
|
||||
(this.settings['CEREBRAS_API_KEY'] as string) ||
|
||||
CEREBRAS_API_KEY
|
||||
|
||||
// Load model from toolkit settings or hardcoded default
|
||||
this.model = (this.settings['CEREBRAS_MODEL'] as string) || CEREBRAS_MODEL
|
||||
|
||||
this.network = new Network({ baseURL: 'https://api.cerebras.ai/v1' })
|
||||
}
|
||||
|
||||
get toolName(): string {
|
||||
return 'cerebras'
|
||||
}
|
||||
|
||||
get toolkit(): string {
|
||||
return CerebrasTool.TOOLKIT
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.config['description']
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Cerebras API key
|
||||
*/
|
||||
setApiKey(apiKey: string): void {
|
||||
this.api_key = apiKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of popular available models
|
||||
*/
|
||||
getAvailableModels(): string[] {
|
||||
return Object.keys(this.popular_models)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert friendly model name to Cerebras model ID
|
||||
*/
|
||||
getModelId(modelName: string): string {
|
||||
return (
|
||||
this.popular_models[modelName as keyof typeof this.popular_models] ||
|
||||
modelName
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat completion request to Cerebras
|
||||
*/
|
||||
async chatCompletion(options: ChatCompletionOptions): Promise<ApiResponse> {
|
||||
const {
|
||||
messages,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output = false,
|
||||
json_schema
|
||||
} = options
|
||||
|
||||
if (!this.api_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cerebras API key not configured'
|
||||
}
|
||||
}
|
||||
|
||||
// Use default model if none provided
|
||||
const finalModel = model || this.model
|
||||
const modelId = this.getModelId(finalModel)
|
||||
|
||||
const requestMessages = []
|
||||
if (system_prompt) {
|
||||
requestMessages.push({ role: 'system', content: system_prompt })
|
||||
}
|
||||
requestMessages.push(...messages)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const payload: any = {
|
||||
model: modelId,
|
||||
messages: requestMessages,
|
||||
temperature
|
||||
}
|
||||
|
||||
if (max_tokens) {
|
||||
payload.max_tokens = max_tokens
|
||||
}
|
||||
|
||||
if (use_structured_output) {
|
||||
payload.response_format = { type: 'json_object' }
|
||||
if (json_schema) {
|
||||
const schemaText = JSON.stringify(json_schema)
|
||||
const schemaPrompt = `You must return a valid JSON object that matches this schema:\n${schemaText}`
|
||||
payload.messages = [
|
||||
{ role: 'system', content: schemaPrompt },
|
||||
...requestMessages
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.network.request({
|
||||
url: '/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: payload
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: response.data as any,
|
||||
model_used: modelId
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Cerebras API error: ${(error as Error).message}`,
|
||||
status_code:
|
||||
error instanceof NetworkError ? error.response.statusCode : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General text completion for any use case
|
||||
*/
|
||||
async completion(options: CompletionOptions): Promise<ApiResponse> {
|
||||
const {
|
||||
prompt,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output = false,
|
||||
json_schema
|
||||
} = options
|
||||
|
||||
const messages = [{ role: 'user', content: prompt }]
|
||||
|
||||
const response = await this.chatCompletion({
|
||||
messages,
|
||||
model: model || this.model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output,
|
||||
json_schema
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0].message.content
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { content },
|
||||
model_used: response.model_used
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to extract completion: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate structured JSON output using Cerebras structured outputs
|
||||
*/
|
||||
async structuredCompletion(
|
||||
options: StructuredCompletionOptions
|
||||
): Promise<ApiResponse> {
|
||||
const {
|
||||
prompt,
|
||||
json_schema,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt
|
||||
} = options
|
||||
|
||||
const messages = [{ role: 'user', content: prompt }]
|
||||
|
||||
const response = await this.chatCompletion({
|
||||
messages,
|
||||
model: model || this.model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output: true,
|
||||
json_schema
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0].message.content
|
||||
const parsedData = JSON.parse(content)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: parsedData,
|
||||
model_used: response.model_used
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SyntaxError) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to parse JSON response: ${error.message}`
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to extract completion: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available models from Cerebras API
|
||||
*/
|
||||
async listModels(): Promise<ApiResponse> {
|
||||
if (!this.api_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Cerebras API key not configured'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.network.request({
|
||||
url: '/models',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: { models: (response.data as any).data }
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to fetch models: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './cerebras-tool'
|
||||
@@ -0,0 +1,290 @@
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from bridges.python.src.sdk.base_tool import BaseTool
|
||||
from bridges.python.src.sdk.toolkit_config import ToolkitConfig
|
||||
from bridges.python.src.sdk.network import Network, NetworkError
|
||||
|
||||
# Hardcoded default settings for Cerebras tool
|
||||
CEREBRAS_API_KEY = None
|
||||
CEREBRAS_MODEL = "zai-glm-4.7"
|
||||
DEFAULT_SETTINGS = {
|
||||
"CEREBRAS_API_KEY": CEREBRAS_API_KEY,
|
||||
"CEREBRAS_MODEL": CEREBRAS_MODEL,
|
||||
}
|
||||
REQUIRED_SETTINGS = ["CEREBRAS_API_KEY"]
|
||||
|
||||
|
||||
class CerebrasTool(BaseTool):
|
||||
"""Cerebras tool for LLM API access (e.g., GLM 4.7)"""
|
||||
|
||||
TOOLKIT = "communication"
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
super().__init__()
|
||||
self.config = ToolkitConfig.load(self.TOOLKIT, self.tool_name)
|
||||
|
||||
tool_settings = ToolkitConfig.load_tool_settings(
|
||||
self.TOOLKIT, self.tool_name, DEFAULT_SETTINGS
|
||||
)
|
||||
self.settings = tool_settings
|
||||
self.required_settings = REQUIRED_SETTINGS
|
||||
self._check_required_settings(self.tool_name)
|
||||
|
||||
# Priority: skill-provided api_key > toolkit settings > hardcoded default
|
||||
self.api_key = api_key or self.settings.get(
|
||||
"CEREBRAS_API_KEY", CEREBRAS_API_KEY
|
||||
)
|
||||
|
||||
# Load model settings
|
||||
self.model = self.settings.get("CEREBRAS_MODEL", CEREBRAS_MODEL)
|
||||
|
||||
self.network = Network({"base_url": "https://api.cerebras.ai/v1"})
|
||||
|
||||
# Popular Cerebras-hosted models (override with full model IDs if needed)
|
||||
self.popular_models = {
|
||||
"zai-glm-4.7": "zai-glm-4.7",
|
||||
"qwen-3-235b-a22b-instruct-2507": "qwen-3-235b-a22b-instruct-2507",
|
||||
"qwen-3-32b": "qwen-3-32b",
|
||||
}
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
return "cerebras"
|
||||
|
||||
@property
|
||||
def toolkit(self) -> str:
|
||||
return self.TOOLKIT
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self.config["description"]
|
||||
|
||||
def set_api_key(self, api_key: str) -> None:
|
||||
"""Set the Cerebras API key"""
|
||||
self.api_key = api_key
|
||||
|
||||
def get_available_models(self) -> List[str]:
|
||||
"""Get list of popular available models"""
|
||||
return list(self.popular_models.keys())
|
||||
|
||||
def get_model_id(self, model_name: str) -> str:
|
||||
"""Convert friendly model name to Cerebras model ID"""
|
||||
return self.popular_models.get(model_name, model_name)
|
||||
|
||||
def chat_completion(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
use_structured_output: bool = False,
|
||||
json_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Send a chat completion request to Cerebras
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries with 'role' and 'content'
|
||||
model: Model name (friendly name or full model ID)
|
||||
temperature: Sampling temperature (0-2)
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: System prompt to prepend
|
||||
use_structured_output: Whether to use structured outputs
|
||||
json_schema: JSON schema for structured output (required if use_structured_output=True)
|
||||
|
||||
Returns:
|
||||
Dict with response data or error information
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {"success": False, "error": "Cerebras API key not configured"}
|
||||
|
||||
# Use default model if none provided
|
||||
model = model or self.model
|
||||
|
||||
model_id = self.get_model_id(model)
|
||||
|
||||
request_messages: List[Dict[str, str]] = []
|
||||
if system_prompt:
|
||||
request_messages.append({"role": "system", "content": system_prompt})
|
||||
request_messages.extend(messages)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"model": model_id,
|
||||
"messages": request_messages,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
if max_tokens:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
if use_structured_output:
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
if json_schema:
|
||||
schema_text = json.dumps(json_schema)
|
||||
schema_prompt = (
|
||||
"You must return a valid JSON object that matches this schema:\n"
|
||||
f"{schema_text}"
|
||||
)
|
||||
payload["messages"] = [
|
||||
{"role": "system", "content": schema_prompt}
|
||||
] + request_messages
|
||||
|
||||
try:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/chat/completions",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"data": payload,
|
||||
}
|
||||
)
|
||||
|
||||
return {"success": True, "data": response["data"], "model_used": model_id}
|
||||
|
||||
except NetworkError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Cerebras API error: {str(e)}",
|
||||
"status_code": getattr(e.response, "status_code", None),
|
||||
}
|
||||
|
||||
def completion(
|
||||
self,
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
use_structured_output: bool = False,
|
||||
json_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
General text completion for any use case
|
||||
|
||||
Args:
|
||||
prompt: Text prompt to complete
|
||||
model: LLM model to use
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: Optional system prompt
|
||||
use_structured_output: Whether to use structured outputs
|
||||
json_schema: JSON schema for structured output
|
||||
|
||||
Returns:
|
||||
Dict with completion result
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
response = self.chat_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
use_structured_output=use_structured_output,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
return response
|
||||
|
||||
try:
|
||||
content = response["data"]["choices"][0]["message"]["content"]
|
||||
return {
|
||||
"success": True,
|
||||
"content": content,
|
||||
"model_used": response["model_used"],
|
||||
}
|
||||
except (KeyError, IndexError) as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to extract completion: {str(e)}",
|
||||
}
|
||||
|
||||
def structured_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
json_schema: Dict[str, Any],
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate structured JSON output using Cerebras structured outputs
|
||||
|
||||
Args:
|
||||
prompt: Text prompt to complete
|
||||
json_schema: JSON schema defining the required output structure
|
||||
model: LLM model to use
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: Optional system prompt
|
||||
|
||||
Returns:
|
||||
Dict with parsed JSON result or error
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
response = self.chat_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
use_structured_output=True,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
return response
|
||||
|
||||
try:
|
||||
content = response["data"]["choices"][0]["message"]["content"]
|
||||
parsed_data = json.loads(content)
|
||||
return {
|
||||
"success": True,
|
||||
"data": parsed_data,
|
||||
"model_used": response["model_used"],
|
||||
}
|
||||
except (KeyError, IndexError) as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to extract completion: {str(e)}",
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to parse JSON response: {str(e)}",
|
||||
}
|
||||
|
||||
def list_models(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get list of available models from Cerebras API
|
||||
|
||||
Returns:
|
||||
Dict with models list or error
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {"success": False, "error": "Cerebras API key not configured"}
|
||||
|
||||
try:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/models",
|
||||
"method": "GET",
|
||||
"headers": {"Authorization": f"Bearer {self.api_key}"},
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"models": response["data"].get("data", response["data"]),
|
||||
}
|
||||
except NetworkError as e:
|
||||
return {"success": False, "error": f"Failed to fetch models: {str(e)}"}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"$schema": "../../../schemas/tool-schemas/tool.json",
|
||||
"tool_id": "cerebras",
|
||||
"toolkit_id": "communication",
|
||||
"name": "Cerebras",
|
||||
"description": "A tool for interacting with Cerebras LLM APIs (e.g., GLM 4.7).",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"functions": {
|
||||
"chatCompletion": {
|
||||
"description": "Generate a chat completion using the Cerebras API.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"content"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_structured_output": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"messages"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"completion": {
|
||||
"description": "Generate a completion using the Cerebras API.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_structured_output": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"structuredCompletion": {
|
||||
"description": "Generate a structured completion using a JSON schema.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt",
|
||||
"json_schema"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"listModels": {
|
||||
"description": "List available Cerebras models.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from .src.python.inference_tool import InferenceTool
|
||||
|
||||
__all__ = ["InferenceTool"]
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './inference-tool'
|
||||
@@ -0,0 +1,97 @@
|
||||
import { Tool } from '@sdk/base-tool'
|
||||
import { ToolkitConfig } from '@sdk/toolkit-config'
|
||||
import { Network } from '@sdk/network'
|
||||
|
||||
interface CompletionOptions {
|
||||
prompt: string
|
||||
system_prompt?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
thought_tokens_budget?: number
|
||||
disable_thinking?: boolean
|
||||
reasoning_mode?: 'off' | 'guarded' | 'on'
|
||||
track_provider_errors?: boolean
|
||||
}
|
||||
|
||||
interface StructuredCompletionOptions extends CompletionOptions {
|
||||
json_schema: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface InferenceResponse {
|
||||
success: boolean
|
||||
output?: unknown
|
||||
reasoning?: string
|
||||
usedInputTokens?: number
|
||||
usedOutputTokens?: number
|
||||
generationDurationMs?: number
|
||||
providerDecodeDurationMs?: number
|
||||
providerTokensPerSecond?: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export default class InferenceTool extends Tool {
|
||||
private static readonly TOOLKIT = 'communication'
|
||||
private readonly config: ReturnType<typeof ToolkitConfig.load>
|
||||
private readonly network: Network
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.config = ToolkitConfig.load(InferenceTool.TOOLKIT, this.toolName)
|
||||
this.network = new Network({
|
||||
baseURL: `${process.env['LEON_HOST']}:${process.env['LEON_PORT']}/api/v1`
|
||||
})
|
||||
}
|
||||
|
||||
get toolName(): string {
|
||||
return 'inference'
|
||||
}
|
||||
|
||||
get toolkit(): string {
|
||||
return InferenceTool.TOOLKIT
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.config['description']
|
||||
}
|
||||
|
||||
async completion(options: CompletionOptions): Promise<InferenceResponse> {
|
||||
const response = await this.network.request<InferenceResponse>({
|
||||
url: '/inference',
|
||||
method: 'POST',
|
||||
data: {
|
||||
prompt: options.prompt,
|
||||
systemPrompt: options.system_prompt,
|
||||
temperature: options.temperature,
|
||||
maxTokens: options.max_tokens,
|
||||
thoughtTokensBudget: options.thought_tokens_budget,
|
||||
disableThinking: options.disable_thinking,
|
||||
reasoningMode: options.reasoning_mode,
|
||||
trackProviderErrors: options.track_provider_errors
|
||||
}
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
|
||||
async structuredCompletion(
|
||||
options: StructuredCompletionOptions
|
||||
): Promise<InferenceResponse> {
|
||||
const response = await this.network.request<InferenceResponse>({
|
||||
url: '/inference',
|
||||
method: 'POST',
|
||||
data: {
|
||||
prompt: options.prompt,
|
||||
systemPrompt: options.system_prompt,
|
||||
temperature: options.temperature,
|
||||
maxTokens: options.max_tokens,
|
||||
thoughtTokensBudget: options.thought_tokens_budget,
|
||||
jsonSchema: options.json_schema,
|
||||
disableThinking: options.disable_thinking,
|
||||
reasoningMode: options.reasoning_mode,
|
||||
trackProviderErrors: options.track_provider_errors
|
||||
}
|
||||
})
|
||||
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
from typing import Any, Dict, Optional, Literal
|
||||
|
||||
from bridges.python.src.sdk.base_tool import BaseTool
|
||||
from bridges.python.src.sdk.network import Network
|
||||
from bridges.python.src.sdk.toolkit_config import ToolkitConfig
|
||||
|
||||
|
||||
class InferenceTool(BaseTool):
|
||||
TOOLKIT = "communication"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.config = ToolkitConfig.load(self.TOOLKIT, self.tool_name)
|
||||
self.network = Network(
|
||||
{
|
||||
"base_url": f"{os.environ.get('LEON_HOST')}:{os.environ.get('LEON_PORT')}/api/v1"
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
return "inference"
|
||||
|
||||
@property
|
||||
def toolkit(self) -> str:
|
||||
return self.TOOLKIT
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self.config.get("description", "")
|
||||
|
||||
def completion(
|
||||
self,
|
||||
prompt: str,
|
||||
system_prompt: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
thought_tokens_budget: Optional[int] = None,
|
||||
disable_thinking: Optional[bool] = None,
|
||||
reasoning_mode: Optional[Literal["off", "guarded", "on"]] = None,
|
||||
track_provider_errors: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/inference",
|
||||
"method": "POST",
|
||||
"data": {
|
||||
"prompt": prompt,
|
||||
"systemPrompt": system_prompt,
|
||||
"temperature": temperature,
|
||||
"maxTokens": max_tokens,
|
||||
"thoughtTokensBudget": thought_tokens_budget,
|
||||
"disableThinking": disable_thinking,
|
||||
"reasoningMode": reasoning_mode,
|
||||
"trackProviderErrors": track_provider_errors,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return response["data"]
|
||||
|
||||
def structured_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
json_schema: Dict[str, Any],
|
||||
system_prompt: Optional[str] = None,
|
||||
temperature: Optional[float] = None,
|
||||
max_tokens: Optional[int] = None,
|
||||
thought_tokens_budget: Optional[int] = None,
|
||||
disable_thinking: Optional[bool] = None,
|
||||
reasoning_mode: Optional[Literal["off", "guarded", "on"]] = None,
|
||||
track_provider_errors: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/inference",
|
||||
"method": "POST",
|
||||
"data": {
|
||||
"prompt": prompt,
|
||||
"systemPrompt": system_prompt,
|
||||
"temperature": temperature,
|
||||
"maxTokens": max_tokens,
|
||||
"thoughtTokensBudget": thought_tokens_budget,
|
||||
"jsonSchema": json_schema,
|
||||
"disableThinking": disable_thinking,
|
||||
"reasoningMode": reasoning_mode,
|
||||
"trackProviderErrors": track_provider_errors,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return response["data"]
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"$schema": "../../../schemas/tool-schemas/tool.json",
|
||||
"tool_id": "inference",
|
||||
"toolkit_id": "communication",
|
||||
"name": "Inference",
|
||||
"description": "A generic Leon workflow inference tool backed by the active workflow LLM provider.",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"functions": {
|
||||
"completion": {
|
||||
"description": "Generate a workflow inference completion.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"thought_tokens_budget": {
|
||||
"type": "number"
|
||||
},
|
||||
"disable_thinking": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"track_provider_errors": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt"
|
||||
]
|
||||
}
|
||||
},
|
||||
"structuredCompletion": {
|
||||
"description": "Generate a structured workflow inference completion using a JSON schema.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"thought_tokens_budget": {
|
||||
"type": "number"
|
||||
},
|
||||
"disable_thinking": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning_mode": {
|
||||
"type": "string"
|
||||
},
|
||||
"track_provider_errors": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt",
|
||||
"json_schema"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from .src.python.openrouter_tool import OpenRouterTool
|
||||
|
||||
__all__ = ["OpenRouterTool"]
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './openrouter-tool'
|
||||
@@ -0,0 +1,340 @@
|
||||
import { Tool } from '@sdk/base-tool'
|
||||
import { ToolkitConfig } from '@sdk/toolkit-config'
|
||||
import { Network, NetworkError } from '@sdk/network'
|
||||
|
||||
// Hardcoded default settings for OpenRouter tool
|
||||
const OPENROUTER_API_KEY: string | null = null
|
||||
const OPENROUTER_MODEL = 'google/gemini-3-flash-preview'
|
||||
const DEFAULT_SETTINGS: Record<string, unknown> = {
|
||||
OPENROUTER_API_KEY,
|
||||
OPENROUTER_MODEL
|
||||
}
|
||||
const REQUIRED_SETTINGS = ['OPENROUTER_API_KEY']
|
||||
|
||||
interface ChatMessage {
|
||||
role: string
|
||||
content: string
|
||||
}
|
||||
|
||||
interface ChatCompletionOptions {
|
||||
messages: ChatMessage[]
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
use_structured_output?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema?: Record<string, any>
|
||||
}
|
||||
|
||||
interface CompletionOptions {
|
||||
prompt: string
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
use_structured_output?: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema?: Record<string, any>
|
||||
}
|
||||
|
||||
interface StructuredCompletionOptions {
|
||||
prompt: string
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
json_schema: Record<string, any>
|
||||
model?: string
|
||||
temperature?: number
|
||||
max_tokens?: number
|
||||
system_prompt?: string
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data?: any
|
||||
model_used?: string
|
||||
error?: string
|
||||
status_code?: number
|
||||
}
|
||||
|
||||
export default class OpenRouterTool extends Tool {
|
||||
private static readonly TOOLKIT = 'communication'
|
||||
private readonly config: ReturnType<typeof ToolkitConfig.load>
|
||||
private api_key: string | null
|
||||
private model: string
|
||||
private readonly network: Network
|
||||
|
||||
constructor(apiKey?: string) {
|
||||
super()
|
||||
// Load configuration from central toolkits directory
|
||||
this.config = ToolkitConfig.load(OpenRouterTool.TOOLKIT, this.toolName)
|
||||
|
||||
const toolSettings = ToolkitConfig.loadToolSettings(
|
||||
OpenRouterTool.TOOLKIT,
|
||||
this.toolName,
|
||||
DEFAULT_SETTINGS
|
||||
)
|
||||
this.settings = toolSettings
|
||||
this.requiredSettings = REQUIRED_SETTINGS
|
||||
this.checkRequiredSettings(this.toolName)
|
||||
|
||||
// Priority: skill-provided apiKey > toolkit settings > hardcoded default
|
||||
this.api_key =
|
||||
apiKey ||
|
||||
(this.settings['OPENROUTER_API_KEY'] as string) ||
|
||||
OPENROUTER_API_KEY
|
||||
|
||||
// Load model from toolkit settings or hardcoded default
|
||||
this.model =
|
||||
(this.settings['OPENROUTER_MODEL'] as string) || OPENROUTER_MODEL
|
||||
|
||||
this.network = new Network({ baseURL: 'https://openrouter.ai/api' })
|
||||
}
|
||||
|
||||
get toolName(): string {
|
||||
return 'openrouter'
|
||||
}
|
||||
|
||||
get toolkit(): string {
|
||||
return OpenRouterTool.TOOLKIT
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.config['description']
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the OpenRouter API key
|
||||
*/
|
||||
setApiKey(apiKey: string): void {
|
||||
this.api_key = apiKey
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat completion request to OpenRouter
|
||||
*/
|
||||
async chatCompletion(options: ChatCompletionOptions): Promise<ApiResponse> {
|
||||
const {
|
||||
messages,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output = false,
|
||||
json_schema
|
||||
} = options
|
||||
|
||||
if (!this.api_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'OpenRouter API key not configured'
|
||||
}
|
||||
}
|
||||
|
||||
// Use default model if none provided
|
||||
const finalModel = model || this.model
|
||||
|
||||
// Prepare messages with system prompt if provided
|
||||
const requestMessages = []
|
||||
if (system_prompt) {
|
||||
requestMessages.push({ role: 'system', content: system_prompt })
|
||||
}
|
||||
requestMessages.push(...messages)
|
||||
|
||||
// Prepare request payload
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const payload: any = {
|
||||
model: finalModel,
|
||||
messages: requestMessages,
|
||||
temperature
|
||||
}
|
||||
|
||||
if (max_tokens) {
|
||||
payload.max_tokens = max_tokens
|
||||
}
|
||||
|
||||
// Add structured output configuration if requested
|
||||
if (use_structured_output && json_schema) {
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: json_schema['name'] || 'response',
|
||||
strict: true,
|
||||
schema: json_schema['schema']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.network.request({
|
||||
url: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: payload
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: response.data as any,
|
||||
model_used: finalModel
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `OpenRouter API error: ${(error as Error).message}`,
|
||||
status_code:
|
||||
error instanceof NetworkError ? error.response.statusCode : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* General text completion for any use case
|
||||
*/
|
||||
async completion(options: CompletionOptions): Promise<ApiResponse> {
|
||||
const {
|
||||
prompt,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output = false,
|
||||
json_schema
|
||||
} = options
|
||||
|
||||
const messages = [{ role: 'user', content: prompt }]
|
||||
|
||||
const response = await this.chatCompletion({
|
||||
messages,
|
||||
model: model || this.model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output,
|
||||
json_schema
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0].message.content
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { content },
|
||||
model_used: response.model_used
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to extract completion: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate structured JSON output using OpenRouter's structured outputs feature
|
||||
*/
|
||||
async structuredCompletion(
|
||||
options: StructuredCompletionOptions
|
||||
): Promise<ApiResponse> {
|
||||
const {
|
||||
prompt,
|
||||
json_schema,
|
||||
model,
|
||||
temperature = 0.7,
|
||||
max_tokens,
|
||||
system_prompt
|
||||
} = options
|
||||
|
||||
const messages = [{ role: 'user', content: prompt }]
|
||||
|
||||
const response = await this.chatCompletion({
|
||||
messages,
|
||||
model: model || this.model,
|
||||
temperature,
|
||||
max_tokens,
|
||||
system_prompt,
|
||||
use_structured_output: true,
|
||||
json_schema
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
return response
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0].message.content
|
||||
const parsedData =
|
||||
typeof content === 'string' ? JSON.parse(content) : content
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: parsedData,
|
||||
model_used: response.model_used
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const content = (response.data as any).choices[0]?.message?.content
|
||||
|
||||
if (error instanceof SyntaxError) {
|
||||
// Show raw response preview to help debug JSON parsing errors
|
||||
const preview =
|
||||
typeof content === 'string'
|
||||
? content.substring(0, 500)
|
||||
: JSON.stringify(content ?? 'null').substring(0, 500)
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to parse JSON response: ${error.message}. Response preview: ${preview}`
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to extract completion: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available models from OpenRouter API
|
||||
*/
|
||||
async listModels(): Promise<ApiResponse> {
|
||||
if (!this.api_key) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'OpenRouter API key not configured'
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.network.request({
|
||||
url: '/v1/models',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.api_key}`
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: { models: (response.data as any).data }
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Failed to fetch models: ${(error as Error).message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
from bridges.python.src.sdk.base_tool import BaseTool
|
||||
from bridges.python.src.sdk.toolkit_config import ToolkitConfig
|
||||
from bridges.python.src.sdk.network import Network, NetworkError
|
||||
|
||||
# Hardcoded default settings for OpenRouter tool
|
||||
OPENROUTER_API_KEY = None
|
||||
OPENROUTER_MODEL = "google/gemini-3-flash-preview"
|
||||
DEFAULT_SETTINGS = {
|
||||
"OPENROUTER_API_KEY": OPENROUTER_API_KEY,
|
||||
"OPENROUTER_MODEL": OPENROUTER_MODEL,
|
||||
}
|
||||
REQUIRED_SETTINGS = ["OPENROUTER_API_KEY"]
|
||||
|
||||
|
||||
class OpenRouterTool(BaseTool):
|
||||
"""OpenRouter tool for unified LLM API access across all skills"""
|
||||
|
||||
TOOLKIT = "communication"
|
||||
|
||||
def __init__(self, api_key: Optional[str] = None):
|
||||
super().__init__()
|
||||
self.config = ToolkitConfig.load(self.TOOLKIT, self.tool_name)
|
||||
|
||||
tool_settings = ToolkitConfig.load_tool_settings(
|
||||
self.TOOLKIT, self.tool_name, DEFAULT_SETTINGS
|
||||
)
|
||||
self.settings = tool_settings
|
||||
self.required_settings = REQUIRED_SETTINGS
|
||||
self._check_required_settings(self.tool_name)
|
||||
|
||||
# Priority: skill-provided api_key > toolkit settings > hardcoded default
|
||||
self.api_key = api_key or self.settings.get(
|
||||
"OPENROUTER_API_KEY", OPENROUTER_API_KEY
|
||||
)
|
||||
|
||||
# Load model settings
|
||||
self.model = self.settings.get("OPENROUTER_MODEL", OPENROUTER_MODEL)
|
||||
|
||||
self.network = Network({"base_url": "https://openrouter.ai/api"})
|
||||
|
||||
@property
|
||||
def tool_name(self) -> str:
|
||||
return "openrouter"
|
||||
|
||||
@property
|
||||
def toolkit(self) -> str:
|
||||
return self.TOOLKIT
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
return self.config["description"]
|
||||
|
||||
def set_api_key(self, api_key: str) -> None:
|
||||
"""Set the OpenRouter API key"""
|
||||
self.api_key = api_key
|
||||
|
||||
def chat_completion(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
use_structured_output: bool = False,
|
||||
json_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Send a chat completion request to OpenRouter
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries with 'role' and 'content'
|
||||
model: Model ID (full OpenRouter model ID, e.g. 'google/gemini-3-flash-preview')
|
||||
temperature: Sampling temperature (0-2)
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: System prompt to prepend
|
||||
use_structured_output: Whether to use OpenRouter's structured outputs
|
||||
json_schema: JSON schema for structured output (required if use_structured_output=True)
|
||||
|
||||
Returns:
|
||||
Dict with response data or error information
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {"success": False, "error": "OpenRouter API key not configured"}
|
||||
|
||||
# Use default model if none provided
|
||||
model = model or self.model
|
||||
|
||||
# Prepare messages with system prompt if provided
|
||||
request_messages = []
|
||||
if system_prompt:
|
||||
request_messages.append({"role": "system", "content": system_prompt})
|
||||
request_messages.extend(messages)
|
||||
|
||||
# Prepare request payload
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": request_messages,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
if max_tokens:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
# Add structured output configuration if requested
|
||||
if use_structured_output and json_schema:
|
||||
payload["response_format"] = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": json_schema.get("name", "response"),
|
||||
"strict": True,
|
||||
"schema": json_schema["schema"],
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/v1/chat/completions",
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
"data": payload,
|
||||
}
|
||||
)
|
||||
|
||||
return {"success": True, "data": response["data"], "model_used": model}
|
||||
|
||||
except NetworkError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"OpenRouter API error: {str(e)}",
|
||||
"status_code": getattr(e.response, "status_code", None),
|
||||
}
|
||||
|
||||
def completion(
|
||||
self,
|
||||
prompt: str,
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
use_structured_output: bool = False,
|
||||
json_schema: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
General text completion for any use case
|
||||
|
||||
Args:
|
||||
prompt: Text prompt to complete
|
||||
model: Model ID (full OpenRouter model ID)
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: Optional system prompt
|
||||
use_structured_output: Whether to use structured outputs
|
||||
json_schema: JSON schema for structured output
|
||||
|
||||
Returns:
|
||||
Dict with completion result
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
response = self.chat_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
use_structured_output=use_structured_output,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
return response
|
||||
|
||||
try:
|
||||
content = response["data"]["choices"][0]["message"]["content"]
|
||||
return {
|
||||
"success": True,
|
||||
"content": content,
|
||||
"model_used": response["model_used"],
|
||||
}
|
||||
except (KeyError, IndexError) as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to extract completion: {str(e)}",
|
||||
}
|
||||
|
||||
def structured_completion(
|
||||
self,
|
||||
prompt: str,
|
||||
json_schema: Dict[str, Any],
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.7,
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate structured JSON output using OpenRouter's structured outputs feature
|
||||
|
||||
Args:
|
||||
prompt: Text prompt to complete
|
||||
json_schema: JSON schema defining the required output structure
|
||||
model: Model ID (full OpenRouter model ID)
|
||||
temperature: Sampling temperature
|
||||
max_tokens: Maximum tokens to generate
|
||||
system_prompt: Optional system prompt
|
||||
|
||||
Returns:
|
||||
Dict with parsed JSON result or error
|
||||
"""
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
response = self.chat_completion(
|
||||
messages=messages,
|
||||
model=model,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens,
|
||||
system_prompt=system_prompt,
|
||||
use_structured_output=True,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
if not response["success"]:
|
||||
return response
|
||||
|
||||
try:
|
||||
content = response["data"]["choices"][0]["message"]["content"]
|
||||
# With structured outputs, content is already valid JSON
|
||||
parsed_data = json.loads(content)
|
||||
return {
|
||||
"success": True,
|
||||
"data": parsed_data,
|
||||
"model_used": response["model_used"],
|
||||
}
|
||||
except (KeyError, IndexError) as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to extract completion: {str(e)}",
|
||||
}
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to parse JSON response: {str(e)}",
|
||||
}
|
||||
|
||||
def list_models(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get list of available models from OpenRouter API
|
||||
|
||||
Returns:
|
||||
Dict with models list or error
|
||||
"""
|
||||
if not self.api_key:
|
||||
return {"success": False, "error": "OpenRouter API key not configured"}
|
||||
|
||||
try:
|
||||
response = self.network.request(
|
||||
{
|
||||
"url": "/v1/models",
|
||||
"method": "GET",
|
||||
"headers": {"Authorization": f"Bearer {self.api_key}"},
|
||||
}
|
||||
)
|
||||
|
||||
return {"success": True, "models": response["data"]["data"]}
|
||||
|
||||
except NetworkError as e:
|
||||
return {"success": False, "error": f"Failed to fetch models: {str(e)}"}
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"$schema": "../../../schemas/tool-schemas/tool.json",
|
||||
"tool_id": "openrouter",
|
||||
"toolkit_id": "communication",
|
||||
"name": "OpenRouter",
|
||||
"description": "A tool for interacting with various LLMs through the OpenRouter API gateway.",
|
||||
"icon_name": "route-line",
|
||||
"author": {
|
||||
"name": "Louis Grenard",
|
||||
"email": "louis@getleon.ai",
|
||||
"url": "https://twitter.com/grenlouis"
|
||||
},
|
||||
"functions": {
|
||||
"chatCompletion": {
|
||||
"description": "Generate a chat completion using OpenRouter.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"messages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"role",
|
||||
"content"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_structured_output": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"messages"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"completion": {
|
||||
"description": "Generate a completion using OpenRouter.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"use_structured_output": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"structuredCompletion": {
|
||||
"description": "Generate a structured completion using a JSON schema.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"options": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"json_schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number"
|
||||
},
|
||||
"max_tokens": {
|
||||
"type": "number"
|
||||
},
|
||||
"system_prompt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"prompt",
|
||||
"json_schema"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"options"
|
||||
]
|
||||
}
|
||||
},
|
||||
"listModels": {
|
||||
"description": "List available OpenRouter models.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Communication",
|
||||
"description": "Tools for communication and language model interactions.",
|
||||
"icon_name": "chat-3-line",
|
||||
"context_files": [
|
||||
"LEON.md",
|
||||
"ARCHITECTURE.md",
|
||||
"MEDIA_PROFILE.md"
|
||||
],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Dialog",
|
||||
"description": "Tools for dialog and conversation handling.",
|
||||
"icon_name": "discuss-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "File System",
|
||||
"description": "Tools for file system operations.",
|
||||
"icon_name": "folders-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Food & Drink",
|
||||
"description": "Tools for food and drink queries.",
|
||||
"icon_name": "restaurant-2-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Games",
|
||||
"description": "Tools for games and entertainment.",
|
||||
"icon_name": "gamepad-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Health & Fitness",
|
||||
"description": "Tools for health and fitness information.",
|
||||
"icon_name": "heart-pulse-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Media Generation",
|
||||
"description": "Tools for media generation and creative workflows.",
|
||||
"icon_name": "sparkling-2-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "../../schemas/toolkit-schemas/toolkit.json",
|
||||
"name": "Movies & TV",
|
||||
"description": "Tools for movies and TV information.",
|
||||
"icon_name": "movie-2-line",
|
||||
"context_files": [],
|
||||
"tools": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user