diff --git a/agent-manager/prisma/schema.prisma b/agent-manager/prisma/schema.prisma index 82e592a2..14a1853c 100644 --- a/agent-manager/prisma/schema.prisma +++ b/agent-manager/prisma/schema.prisma @@ -26,6 +26,7 @@ model Agent { triggers Trigger[] // One-to-many relationship with Trigger template Template? @relation(fields: [template_id], references: [id], onDelete: SetNull, onUpdate: Cascade) secret_key Bytes @db.ByteA + config Json? } model Trigger { diff --git a/agent-manager/src/repository/agent_manager_repository.ts b/agent-manager/src/repository/agent_manager_repository.ts index 8173d619..0a45f2cd 100644 --- a/agent-manager/src/repository/agent_manager_repository.ts +++ b/agent-manager/src/repository/agent_manager_repository.ts @@ -24,6 +24,7 @@ export async function fetchAgentConfiguration(agentId: string): Promise< configurations: any[] agentIndex: number agentName: string + agentConfig: any } | undefined > { @@ -46,6 +47,8 @@ export async function fetchAgentConfiguration(agentId: string): Promise< const instanceCount = Number(agentInstance.instance) const agentIndex = Number(agentInstance.index) const agentName = agentInstance.name + const agentConfig = (agentInstance as any).config || null + const configurationsData = agentConfigurations.map( (config: { id: string; type: string; data: JsonValue; action: JsonValue }) => ({ id: config.id, @@ -55,7 +58,7 @@ export async function fetchAgentConfiguration(agentId: string): Promise< }) ) - return { instanceCount, configurations: configurationsData, agentIndex, agentName } + return { instanceCount, configurations: configurationsData, agentIndex, agentName, agentConfig } } } catch (error: any) { console.log(`Error fetching agent configuration: ${error}`) diff --git a/agent-node/.env.example b/agent-node/.env.example index 02499e2f..2b5b0a7f 100644 --- a/agent-node/.env.example +++ b/agent-node/.env.example @@ -1,2 +1,3 @@ TOKEN= -WS_URL=ws://localhost:3001 \ No newline at end of file +WS_URL=ws://localhost:3001 +GEMINI_API_KEY= \ No newline at end of file diff --git a/agent-node/package.json b/agent-node/package.json index 57e14cd2..9f537e75 100644 --- a/agent-node/package.json +++ b/agent-node/package.json @@ -18,6 +18,7 @@ "description": "", "dependencies": { "@emurgo/cardano-serialization-lib-asmjs": "^11.5.0", + "@google/genai": "^1.13.0", "@types/ws": "^8.5.10", "axios": "^1.6.8", "bech32": "^2.0.0", @@ -29,8 +30,8 @@ "ws": "^8.18.0" }, "devDependencies": { - "@types/luxon": "^3.4.2", "@eslint/js": "^9.4.0", + "@types/luxon": "^3.4.2", "@types/node-cron": "^3.0.11", "@types/websocket": "^1.0.10", "eslint": "8", diff --git a/agent-node/src/constants/global.ts b/agent-node/src/constants/global.ts index b6685894..bd63ee81 100644 --- a/agent-node/src/constants/global.ts +++ b/agent-node/src/constants/global.ts @@ -3,9 +3,16 @@ import { IEventBasedAction } from '../types/eventTriger' export const globalState: { eventTriggerTypeDetails: IEventBasedAction[] agentName: string + systemPrompt:string + functionLLMSettings: Record< + string, + { enabled: boolean; userPrefText: string; prefs?: any } + > } = { eventTriggerTypeDetails: [], agentName: '', + systemPrompt:'', + functionLLMSettings: {} } export const globalRootKeyBuffer: { value: Buffer | null } = { diff --git a/agent-node/src/executor/AgentRunner.ts b/agent-node/src/executor/AgentRunner.ts index b19ab090..597e14cd 100644 --- a/agent-node/src/executor/AgentRunner.ts +++ b/agent-node/src/executor/AgentRunner.ts @@ -18,7 +18,7 @@ export class AgentRunner { this.executor = new Executor(null, managerInterface, txListener) } - invokeFunction(triggerType: TriggerType, instanceIndex: number, method: string, ...args: any) { + async invokeFunction(triggerType: TriggerType, instanceIndex: number, method: string, ...args: any) { this.executor.invokeFunction(method, ...args).then((result) => { saveTxLog(result, this.managerInterface, triggerType, instanceIndex) }) diff --git a/agent-node/src/executor/LLMGatedRunner.ts b/agent-node/src/executor/LLMGatedRunner.ts new file mode 100644 index 00000000..2f30b277 --- /dev/null +++ b/agent-node/src/executor/LLMGatedRunner.ts @@ -0,0 +1,89 @@ +import { TriggerType } from '../service/triggerService' +import { saveTxLog } from '../utils/agent' +import { globalState } from '../constants/global' +import { LLMService } from '../service/LLMService' +import { EventContext } from './BaseFunction' +import { AgentRunner } from './AgentRunner' + +export class LLMGatedRunner { + constructor(private readonly core: AgentRunner) {} + + async invokeFunction(triggerType: TriggerType, instanceIndex: number, method: string, ...args: any) { + const extractedArgs = this.extractArgumentValues(args) + const shouldGate = this.shouldUseLLMForFunction(method) && this.isCron(triggerType) + if (shouldGate) { + try { + const llm = new LLMService() + const decision = await llm.shouldExecuteFunction( + method, + extractedArgs, + {}, + this.getUserPreferenceText(method), + this.getSystemPrompt() + ) + if (!decision.should_execute) { + const blocked = [ + { + function: method, + arguments: args, + return: { + operation: method, + executed: false, + blocked_by_llm: true, + llm_reasoning: decision.reasoning, + llm_confidence: decision.confidence, + message: `LLM blocked: ${decision.reasoning}`, + timestamp: new Date().toISOString(), + }, + }, + ] + saveTxLog(blocked, (this.core as any).managerInterface, triggerType, instanceIndex) + return + } + } catch (e) { + console.error(`LLM gating failed, continuing: ${e}`) + } + } + return this.core.invokeFunction(triggerType, instanceIndex, method, ...args) + } + + async invokeFunctionWithEventContext( + eventFilterContext: any, + context: EventContext, + triggerType: TriggerType, + instanceIndex: number, + method: string, + parameters: any[] + ) { + return this.core.invokeFunctionWithEventContext( + eventFilterContext, + context, + triggerType, + instanceIndex, + method, + parameters + ) + } + + async remakeContext(index: number) { + return this.core.remakeContext(index) + } + + // helpers + private isCron(triggerType: TriggerType): boolean { + return String(triggerType) === 'CRON' + } + private shouldUseLLMForFunction(method: string): boolean { + const fnCfg = globalState.functionLLMSettings?.[method] + return !!(fnCfg && fnCfg.enabled) + } + private getUserPreferenceText(method: string): string { + return globalState.functionLLMSettings?.[method]?.userPrefText || '' + } + private getSystemPrompt(): string { + return (globalState.systemPrompt ?? '').toString() + } + private extractArgumentValues(args: any[]) { + return args.map((a) => (a && typeof a === 'object' && 'value' in a ? a.value : a)) + } +} diff --git a/agent-node/src/index.ts b/agent-node/src/index.ts index 0e61049e..65a83add 100644 --- a/agent-node/src/index.ts +++ b/agent-node/src/index.ts @@ -14,6 +14,7 @@ import { AgentRunner } from './executor/AgentRunner' import { decodeBase64string } from './utils/base64converter' import { validateToken } from './utils/validator' import { getHandlers } from './executor/AgentFunctions' +import { LLMGatedRunner } from './executor/LlmGatedRunner' configDotenv() let wsUrl: string = process.env.WS_URL as string @@ -62,7 +63,7 @@ function connectToManagerWebSocket() { const managerInterface = new ManagerInterface(rpcChannel) const txListener = new TxListener() - const agentRunners: Array = [] + const agentRunners: Array = [] rpcChannel.on('methodCall', (method, args) => { agentRunners.forEach((runner, index) => { @@ -71,14 +72,58 @@ function connectToManagerWebSocket() { }) const topicHandler = new RpcTopicHandler(managerInterface, txListener) + + // LLM settings extractor from configurations + function applyFnSettingsFromConfigurations(message: any) { + if (!message?.configurations) return + globalState.functionLLMSettings = {} + message.configurations.forEach((cfg: any) => { + const act = cfg?.action || {} + if (act.function_name) { + globalState.functionLLMSettings[act.function_name] = { + enabled: !!act.llm_enabled, + userPrefText: act.llm_user_preferences_text || '', + prefs: act.llm_preferences || undefined, + } + } + }) + console.log('[INIT] LLM settings for:', Object.keys(globalState.functionLLMSettings)) + } + + // loads the system prompt + function applySystemPromptFromMessage(message: any, logCtx: string) { + const prompt = message?.agentConfig?.system_prompt ?? message?.config?.system_prompt + + if (typeof prompt === 'string' && prompt.length && prompt !== globalState.systemPrompt) { + globalState.systemPrompt = prompt + } + } + rpcChannel.on('event', (topic, message) => { + // initial payload containing configs + if (topic === 'initial_config') { + applySystemPromptFromMessage(message, 'initial_config') + applyFnSettingsFromConfigurations(message) + return + } + + // config updates from manager + if (topic === 'config_updated') { + applyFnSettingsFromConfigurations(message) + applySystemPromptFromMessage(message, 'initial_config') + } + if (topic == 'instance_count') { + applySystemPromptFromMessage(message, 'initial_config') + applyFnSettingsFromConfigurations(message) + globalRootKeyBuffer.value = message.rootKeyBuffer globalState.agentName = message.agentName Array(message.instanceCount) .fill('') .forEach(async (item, index) => { - const runner = new AgentRunner(managerInterface, txListener) + const coreRunner = new AgentRunner(managerInterface, txListener) + const runner = new LLMGatedRunner(coreRunner) await runner.remakeContext(index) agentRunners.push(runner) }) diff --git a/agent-node/src/service/LLMService.ts b/agent-node/src/service/LLMService.ts new file mode 100644 index 00000000..e7b22b4d --- /dev/null +++ b/agent-node/src/service/LLMService.ts @@ -0,0 +1,127 @@ +import 'dotenv/config' +import { GoogleGenAI } from '@google/genai' + +const ai = new GoogleGenAI({}) + +export class LLMService { + private apiKey: string + + constructor() { + this.apiKey = process.env.GEMINI_API_KEY || '' + if (!this.apiKey) { + console.warn('No Gemini API key') + } + } + + async shouldExecuteFunction( + functionName: string, + functionArgs: any[], + structuredPreferences: any, + userPreferenceText: any, + systemPrompt: string + ): Promise<{ + should_execute: boolean + confidence: number + reasoning: string + }> { + if (!this.apiKey) { + console.log('LLM not configured') + return { + should_execute: true, + confidence: 0.5, + reasoning: 'LLM not configured, default allow', + } + } + + try { + const prompt = this.buildPrompt( + functionName, + functionArgs, + structuredPreferences, + userPreferenceText, + systemPrompt + ) + + console.log('Asking LLM...') + + const response: any = await ai.models.generateContent({ + model: 'gemini-2.5-flash', + contents: prompt, + }) + + console.log('Response from Gemini:', response.text) + + const decision = this.extractJson(response.text) + console.log('After parsing:', decision) + + return { + should_execute: decision.should_execute, + confidence: decision.confidence, + reasoning: decision.reasoning, + } + } catch (error: any) { + console.error('LLM failed, error:', error) + return { + should_execute: true, + confidence: 0.6, + reasoning: `LLM service failed: ${error.message}`, + } + } + } + + private buildPrompt( + functionName: string, + functionArgs: any[], + structuredPreferences: any, + userPreferenceText: any, + systemPrompt: string + ): string { + const baseSystemP = systemPrompt || 'You are a Cardano autonomous agent' + console.log('System prompt check:', systemPrompt) + + const context = + structuredPreferences && Object.keys(structuredPreferences).length + ? `\nContext:\n${JSON.stringify(structuredPreferences, null, 2)} ` + : '' + + const userPolicy = userPreferenceText + ? `\nUser Policy:\n${userPreferenceText}` + : '' + + console.log('User policy:', userPolicy) + console.log('Context:', context) + + return ` +${baseSystemP} + +FUNCTION TO EXECUTE: ${functionName} +Args: ${JSON.stringify(functionArgs)}${context}${userPolicy} + +Analyze this call strictly against "User Policy" and System prompt. +Return ONLY JSON: +{"should_execute": true/false, "confidence": 0.0-1.0, "reasoning": "brief"} +` + } + + extractJson(text: string): any { + const cleaned = text.replace(/```json|```/g, '').trim() + + const start = cleaned.indexOf('{') + const end = cleaned.lastIndexOf('}') + + if (start !== -1 && end !== -1) { + const jsonString = cleaned.substring(start, end + 1) + try { + return JSON.parse(jsonString) + } catch (e) { + console.error('JSON parse error:', e, jsonString) + } + } + + return { + should_execute: false, + confidence: 0.0, + reasoning: 'Failed to parse LLM response', + } + } +} diff --git a/agent-node/src/utils/agent.ts b/agent-node/src/utils/agent.ts index 9ac3fa38..cc326aef 100644 --- a/agent-node/src/utils/agent.ts +++ b/agent-node/src/utils/agent.ts @@ -56,7 +56,20 @@ export function saveTxLog( } if (mainLog.return) { txLog.result = mainLog.return - txLog.txHash = mainLog.return.hash + if((mainLog.return as any).blocked_by_llm === true){ + txLog.success = false + txLog.message = + (mainLog.return as any).llm_reasoning || + (mainLog.return as any).message || + 'LLM blocked execution' + } else if ((mainLog.return as any).hash){ + txLog.txHash = (mainLog.return as any).hash + txLog.message = (mainLog.return as any).message || 'Function executed successfully' + + } else { + txLog.message = (mainLog.return as any).message || 'Function executed successfully' + } + } else if (mainLog.error) { txLog.result = mainLog.error txLog.message = mainLog.error && ((mainLog.error as Error).message ?? mainLog.error) @@ -72,7 +85,20 @@ export function saveTxLog( } if (log.return) { internalLog.result = log.return - internalLog.txHash = log.return.hash + if ((log.return as any).blocked_by_llm === true) { + internalLog.success = false + internalLog.message = + (log.return as any).llm_reasoning || + (log.return as any).message || + 'LLM blocked execution' + } else if ((log.return as any).hash) { + internalLog.txHash = (log.return as any).hash + internalLog.message = (log.return as any).message || 'Function executed successfully' + } else { + internalLog.message = (log.return as any).message || 'Function executed successfully' + } + + } else if (log.error) { internalLog.result = log.error internalLog.message = log.error && (log.error.message ?? log.error) diff --git a/agent-node/yarn.lock b/agent-node/yarn.lock index d334ef00..ed4dba57 100644 --- a/agent-node/yarn.lock +++ b/agent-node/yarn.lock @@ -93,6 +93,14 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== +"@google/genai@^1.13.0": + version "1.16.0" + resolved "https://registry.yarnpkg.com/@google/genai/-/genai-1.16.0.tgz#b4b88203881249e31ae726bc8d61fabd2712a47c" + integrity sha512-hdTYu39QgDFxv+FB6BK2zi4UIJGWhx2iPc0pHQ0C5Q/RCi+m+4gsryIzTGO+riqWcUA8/WGYp6hpqckdOBNysw== + dependencies: + google-auth-library "^9.14.2" + ws "^8.18.0" + "@humanwhocodes/config-array@^0.13.0": version "0.13.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" @@ -593,6 +601,11 @@ agent-base@6, agent-base@^6.0.2: dependencies: debug "4" +agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + agentkeepalive@^4.2.1: version "4.5.0" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" @@ -727,11 +740,21 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + bech32@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== +bignumber.js@^9.0.0: + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== + bin-links@^4.0.1: version "4.0.4" resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-4.0.4.tgz#c3565832b8e287c85f109a02a17027d152a58a63" @@ -846,6 +869,11 @@ buffer-crc32@~0.2.5: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== +buffer-equal-constant-time@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== + buffer-fill@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" @@ -1205,6 +1233,13 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -1401,6 +1436,11 @@ ext@^1.7.0: dependencies: type "^2.7.2" +extend@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -1573,6 +1613,26 @@ gauge@~2.7.3: strip-ansi "^3.0.1" wide-align "^1.1.0" +gaxios@^6.0.0, gaxios@^6.1.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.7.1.tgz#ebd9f7093ede3ba502685e73390248bb5b7f71fb" + integrity sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ== + dependencies: + extend "^3.0.2" + https-proxy-agent "^7.0.1" + is-stream "^2.0.0" + node-fetch "^2.6.9" + uuid "^9.0.1" + +gcp-metadata@^6.1.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.1.tgz#f65aa69f546bc56e116061d137d3f5f90bdec494" + integrity sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A== + dependencies: + gaxios "^6.1.1" + google-logging-utils "^0.0.2" + json-bigint "^1.0.0" + github-from-package@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" @@ -1651,6 +1711,23 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +google-auth-library@^9.14.2: + version "9.15.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.15.1.tgz#0c5d84ed1890b2375f1cd74f03ac7b806b392928" + integrity sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng== + dependencies: + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + gaxios "^6.1.1" + gcp-metadata "^6.1.0" + gtoken "^7.0.0" + jws "^4.0.0" + +google-logging-utils@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/google-logging-utils/-/google-logging-utils-0.0.2.tgz#5fd837e06fa334da450433b9e3e1870c1594466a" + integrity sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ== + graceful-fs@^4.2.11, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -1661,6 +1738,14 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +gtoken@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" + integrity sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw== + dependencies: + gaxios "^6.0.0" + jws "^4.0.0" + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -1716,6 +1801,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.1: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -1872,6 +1965,11 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + is-typedarray@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -1908,6 +2006,13 @@ jsbn@1.1.0: resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -1953,6 +2058,23 @@ just-diff@^6.0.0: resolved "https://registry.yarnpkg.com/just-diff/-/just-diff-6.0.2.tgz#03b65908543ac0521caf6d8eb85035f7d27ea285" integrity sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA== +jwa@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.1.tgz#bf8176d1ad0cd72e0f3f58338595a13e110bc804" + integrity sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg== + dependencies: + buffer-equal-constant-time "^1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + kafka-node@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/kafka-node/-/kafka-node-5.0.0.tgz#4b6f65cc1d77ebe565859dfb8f9575ed15d543c0" @@ -2465,6 +2587,13 @@ node-cron@^3.0.3: dependencies: uuid "8.3.2" +node-fetch@^2.6.9: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-gyp-build@^4.3.0: version "4.8.4" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" @@ -3278,7 +3407,7 @@ stream-browserify@^3.0.0: inherits "~2.0.4" readable-stream "^3.5.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -3296,6 +3425,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -3319,7 +3457,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -3333,6 +3471,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -3424,6 +3569,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" @@ -3598,6 +3748,11 @@ uuid@^3.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -3628,6 +3783,11 @@ wcwidth@^1.0.0: dependencies: defaults "^1.0.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + websocket@^1.0.35: version "1.0.35" resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.35.tgz#374197207d7d4cc4c36cbf8a1bb886ee52a07885" @@ -3640,6 +3800,14 @@ websocket@^1.0.35: utf-8-validate "^5.0.2" yaeti "^0.0.6" +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-pm-runs@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.1.0.tgz#35ccf7b1a0fce87bd8b92a478c9d045785d3bf35" diff --git a/api/backend/app/models/agent/agent_config.py b/api/backend/app/models/agent/agent_config.py new file mode 100644 index 00000000..230e3fc0 --- /dev/null +++ b/api/backend/app/models/agent/agent_config.py @@ -0,0 +1,9 @@ +from typing import Optional +from pydantic import BaseModel, Field + + +class AgentConfig(BaseModel): + system_prompt: Optional[str] = Field(default=None, description="LLM system prompt for this agent") + + class Config: + extra = "allow" diff --git a/api/backend/app/models/agent/agent_dto.py b/api/backend/app/models/agent/agent_dto.py index 536e8a60..a6c06160 100644 --- a/api/backend/app/models/agent/agent_dto.py +++ b/api/backend/app/models/agent/agent_dto.py @@ -1,9 +1,10 @@ from datetime import datetime from pydantic import BaseModel, Field -from typing import List, Optional +from typing import List, Optional, Dict from backend.app.models import TriggerResponse +from backend.app.models.agent.agent_config import AgentConfig class AgentCreateDTO(BaseModel): @@ -19,3 +20,4 @@ class AgentUpdateDTO(BaseModel): template_id: Optional[str] agent_configurations: Optional[List[TriggerResponse]] instance: int = Field() + config: Optional[AgentConfig] = None diff --git a/api/backend/app/models/agent/response_dto.py b/api/backend/app/models/agent/response_dto.py index 8a84507e..6b03467e 100644 --- a/api/backend/app/models/agent/response_dto.py +++ b/api/backend/app/models/agent/response_dto.py @@ -4,6 +4,7 @@ from pydantic import BaseModel from backend.app.models import TriggerResponse +from backend.app.models.agent.agent_config import AgentConfig class AgentResponse(BaseModel): @@ -20,6 +21,7 @@ class AgentResponse(BaseModel): is_drep_registered: Optional[bool] no_of_successfull_triggers: Optional[int] secret_key: Optional[str] + config: Optional[AgentConfig] = None class AgentResponseWithAgentConfigurations(AgentResponse): diff --git a/api/backend/app/models/trigger/trigger_dto.py b/api/backend/app/models/trigger/trigger_dto.py index 3a43bcd4..651fa102 100644 --- a/api/backend/app/models/trigger/trigger_dto.py +++ b/api/backend/app/models/trigger/trigger_dto.py @@ -21,6 +21,8 @@ class SubParameter(BaseModel): class Action(BaseModel): function_name: str parameters: list[SubParameter] + llm_enabled: Optional[bool] = None + llm_user_preferences_text: Optional[str] = None # For Event Based Trigger diff --git a/api/backend/app/repositories/agent_repository.py b/api/backend/app/repositories/agent_repository.py index 45f636a5..421dd235 100644 --- a/api/backend/app/repositories/agent_repository.py +++ b/api/backend/app/repositories/agent_repository.py @@ -6,6 +6,8 @@ import uuid from datetime import datetime, timezone, timedelta, UTC from typing import List, Optional +from prisma import Json +from pydantic import BaseModel import pycardano from pycardano import ( @@ -24,6 +26,17 @@ from backend.config.database import prisma_connection +# Convert to a JSON-serializable dict +def _to_db_json(value): + if value is None: + return None + if isinstance(value, BaseModel): + return value.model_dump(exclude_none=True) if hasattr(value, "model_dump") else value.dict(exclude_none=True) + if isinstance(value, dict): + return value + return json.loads(json.dumps(value)) + + class AgentRepository: def __init__(self, db_connection=None): self.db = db_connection or prisma_connection @@ -85,6 +98,7 @@ async def retrieve_agent( is_drep_registered=agent.is_drep_registered, no_of_successfull_triggers=successful_triggers, secret_key=str(agent.secret_key) if display_secret_key else None, + config=(json.loads(json.dumps(agent.config)) if agent.config is not None else None), ) return agent_response @@ -97,6 +111,13 @@ async def modify_agent(self, agent_id: str, agent_data: AgentUpdateDTO) -> Optio "instance": agent_data.instance, "updated_at": datetime.now(timezone.utc), } + + if hasattr(agent_data, "config"): + safe_config = _to_db_json(agent_data.config) + updated_data["config"] = Json(safe_config) + else: + print("no config in the agent_data") + updated_agent = await self.db.prisma.agent.update(where={"id": agent_id}, data=updated_data) updated_agent.secret_key = str(updated_agent.secret_key) return updated_agent diff --git a/api/prisma/migrations/20250828090531_add_agent_config/migration.sql b/api/prisma/migrations/20250828090531_add_agent_config/migration.sql new file mode 100644 index 00000000..980ee690 --- /dev/null +++ b/api/prisma/migrations/20250828090531_add_agent_config/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."Agent" ADD COLUMN "config" JSONB; diff --git a/api/prisma/migrations/migration_lock.toml b/api/prisma/migrations/migration_lock.toml index fbffa92c..044d57cd 100644 --- a/api/prisma/migrations/migration_lock.toml +++ b/api/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index a15f62af..03c863f9 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -27,6 +27,7 @@ model Agent { triggers Trigger[] // One-to-many relationship with Trigger template Template? @relation(fields: [template_id], references: [id], onDelete: SetNull, onUpdate: Cascade) secret_key Bytes @db.ByteA + config Json? } model Trigger { diff --git a/frontend/src/api/agents.ts b/frontend/src/api/agents.ts index 47c20ecb..e31fe07a 100644 --- a/frontend/src/api/agents.ts +++ b/frontend/src/api/agents.ts @@ -47,6 +47,9 @@ export interface ISubParameter { export interface IAgentAction { function_name: string; parameters: Array; + llm_enabled?: boolean + llm_user_preferences_text?: string + } export interface ICronTrigger { @@ -68,6 +71,7 @@ export interface IAgentUpdateReqDto { templateId?: string; agentConfigurations?: Array; instance?: number; + agentConfig?: { system_prompt?: string }; } export interface IDelegation { @@ -98,6 +102,7 @@ export interface IAgent { stake_last_registered?: string; no_of_successfull_triggers?: number; secret_key?: string; + config?: {system_prompt?: string} } export const fetchAgents = async (params: { page: number; size: number; search: string }): Promise => { @@ -153,7 +158,8 @@ export const updateAgentData = async (formData: IAgentUpdateReqDto) => { name: formData.agentName, template_id: formData.templateId, instance: formData.instance, - agent_configurations: formData.agentConfigurations + agent_configurations: formData.agentConfigurations, + config: formData.agentConfig }, { headers: { diff --git a/frontend/src/api/templates.ts b/frontend/src/api/templates.ts index 3024f605..2515d497 100644 --- a/frontend/src/api/templates.ts +++ b/frontend/src/api/templates.ts @@ -31,6 +31,7 @@ export interface ITemplate { name: string; description: string; template_configurations?: Array; + config?: {system_prompt?:string}; } export interface ICreateTemplateRequestDTO { diff --git a/frontend/src/app/(pages)/templates/create-template/components/FunctionForm.tsx b/frontend/src/app/(pages)/templates/create-template/components/FunctionForm.tsx index f103c3bc..441e85aa 100644 --- a/frontend/src/app/(pages)/templates/create-template/components/FunctionForm.tsx +++ b/frontend/src/app/(pages)/templates/create-template/components/FunctionForm.tsx @@ -12,6 +12,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger } from '@app/component import { cn } from '@app/components/lib/utils'; import { CustomCombobox } from '@app/components/molecules/CustomCombobox'; import { ErrorToast } from '@app/components/molecules/CustomToasts'; +import { Switch } from '@app/components/shadcn/ui/switch'; import { IFormFunctionInstance } from '../page'; import { renderParameters } from './ParameterRenderers'; @@ -41,7 +42,8 @@ export const FunctionForm = ({ currentFunction || { ...TemplateFunctions[0], index: '0', - type: 'CRON' as TriggerType + type: 'CRON' as TriggerType, + action: { llm_enabled: false, llm_user_preferences_text: '' } } ); const [cronExpression] = useState(currentFunction?.cronValue?.frequency || '* * * * *'); @@ -106,6 +108,18 @@ export const FunctionForm = ({ updateFunctionState(updatedFunctionState); }; + const handleLlmEnabledChange = (enabled: boolean) => { + updateFunctionState({ + action: { ...(functionState.action || {}), llm_enabled: enabled } + }); + }; + + const handleLlmPreferencesChange = (text: string) => { + updateFunctionState({ + action: { ...(functionState.action || {}), llm_user_preferences_text: text } + }); + }; + const handleOnSave = () => { const validState = checkAllRequiredFieldsAreFilled(); validState && onSave?.(functionState); @@ -243,6 +257,30 @@ export const FunctionForm = ({ } /> + +
+
+ Enable LLM + +
+ {functionState.action?.llm_enabled && ( +
+ LLM Preferences +