Skip to content

Commit bcf01cc

Browse files
committed
feat(llm): add LLM gating
1 parent b74d5fb commit bcf01cc

File tree

24 files changed

+602
-23
lines changed

24 files changed

+602
-23
lines changed

agent-manager/src/repository/agent_manager_repository.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export async function fetchAgentConfiguration(agentId: string): Promise<
2424
configurations: any[]
2525
agentIndex: number
2626
agentName: string
27+
agentConfig: any
2728
}
2829
| undefined
2930
> {
@@ -46,6 +47,8 @@ export async function fetchAgentConfiguration(agentId: string): Promise<
4647
const instanceCount = Number(agentInstance.instance)
4748
const agentIndex = Number(agentInstance.index)
4849
const agentName = agentInstance.name
50+
const agentConfig = (agentInstance as any).config || null
51+
4952
const configurationsData = agentConfigurations.map(
5053
(config: { id: string; type: string; data: JsonValue; action: JsonValue }) => ({
5154
id: config.id,
@@ -55,7 +58,7 @@ export async function fetchAgentConfiguration(agentId: string): Promise<
5558
})
5659
)
5760

58-
return { instanceCount, configurations: configurationsData, agentIndex, agentName }
61+
return { instanceCount, configurations: configurationsData, agentIndex, agentName, agentConfig }
5962
}
6063
} catch (error: any) {
6164
console.log(`Error fetching agent configuration: ${error}`)

agent-node/.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
TOKEN=
2-
WS_URL=ws://localhost:3001
2+
WS_URL=ws://localhost:3001
3+
GEMINI_API_KEY=

agent-node/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"description": "",
1919
"dependencies": {
2020
"@emurgo/cardano-serialization-lib-asmjs": "^11.5.0",
21+
"@google/genai": "^1.13.0",
2122
"@types/ws": "^8.5.10",
2223
"axios": "^1.6.8",
2324
"bech32": "^2.0.0",
@@ -29,8 +30,8 @@
2930
"ws": "^8.18.0"
3031
},
3132
"devDependencies": {
32-
"@types/luxon": "^3.4.2",
3333
"@eslint/js": "^9.4.0",
34+
"@types/luxon": "^3.4.2",
3435
"@types/node-cron": "^3.0.11",
3536
"@types/websocket": "^1.0.10",
3637
"eslint": "8",

agent-node/src/constants/global.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@ import { IEventBasedAction } from '../types/eventTriger'
33
export const globalState: {
44
eventTriggerTypeDetails: IEventBasedAction[]
55
agentName: string
6+
systemPrompt:string
7+
functionLLMSettings: Record<
8+
string,
9+
{ enabled: boolean; userPrefText: string; prefs?: any }
10+
>
611
} = {
712
eventTriggerTypeDetails: [],
813
agentName: '',
14+
systemPrompt:'',
15+
functionLLMSettings: {}
916
}
1017

1118
export const globalRootKeyBuffer: { value: Buffer | null } = {

agent-node/src/executor/AgentRunner.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { HdWallet } from 'libcardano'
88
import { AgentWalletDetails } from '../types/types'
99
import { globalState } from '../constants/global'
1010
import { EventContext } from './BaseFunction'
11+
import { LLMService } from '../service/LLMService'
1112

1213
export class AgentRunner {
1314
executor: Executor
@@ -18,7 +19,61 @@ export class AgentRunner {
1819
this.executor = new Executor(null, managerInterface, txListener)
1920
}
2021

21-
invokeFunction(triggerType: TriggerType, instanceIndex: number, method: string, ...args: any) {
22+
async invokeFunction(triggerType: TriggerType, instanceIndex: number, method: string, ...args: any) {
23+
const extractedArgs = this.extractArgumentValues(args)
24+
const shouldUseLLM = this.shouldUseLLMForFunction(method)
25+
26+
if (shouldUseLLM) {
27+
console.log('[AgentRunner] LLM gating enabled for', method)
28+
console.log(
29+
'[AgentRunner] GEMINI_API_KEY present:',
30+
!!process.env.GEMINI_API_KEY || !!process.env.GOOGLE_API_KEY
31+
)
32+
try {
33+
const llm = new LLMService()
34+
console.log(
35+
'[AgentRunner] functionLLMSettings:',
36+
JSON.stringify(globalState.functionLLMSettings, null, 2)
37+
)
38+
const userPrefText = this.getUserPreferenceText(method)
39+
console.log(
40+
'[AgentRunner] user policy for',
41+
method,
42+
':',
43+
userPrefText || '(empty)'
44+
)
45+
const structuredPrefs = {}
46+
const decision = await llm.shouldExecuteFunction(
47+
method,
48+
extractedArgs,
49+
structuredPrefs,
50+
userPrefText,
51+
globalState.systemPrompt
52+
)
53+
if (!decision.should_execute) {
54+
const blocked = [
55+
{
56+
function: method,
57+
arguments: args,
58+
return: {
59+
operation: method,
60+
executed: false,
61+
blocked_by_llm: true,
62+
llm_reasoning: decision.reasoning,
63+
llm_confidence: decision.confidence,
64+
message: `LLM blocked: ${decision.reasoning}`,
65+
timestamp: new Date().toISOString(),
66+
},
67+
},
68+
]
69+
saveTxLog(blocked, this.managerInterface, triggerType, instanceIndex)
70+
return
71+
}
72+
} catch (e) {
73+
console.error(`LLM gating failed, continuing: ${e}`)
74+
}
75+
}
76+
2277
this.executor.invokeFunction(method, ...args).then((result) => {
2378
saveTxLog(result, this.managerInterface, triggerType, instanceIndex)
2479
})
@@ -47,6 +102,27 @@ export class AgentRunner {
47102
})
48103
}
49104
}
105+
106+
// LLM helpers
107+
private shouldUseLLMForFunction(method: string): boolean {
108+
const fnCfg = globalState.functionLLMSettings?.[method]
109+
console.log('fncfg is', fnCfg)
110+
return !!(fnCfg && fnCfg.enabled)
111+
}
112+
113+
private getUserPreferenceText(method: string): string {
114+
console.log(
115+
`globalState function llm settings -> ${JSON.stringify(
116+
globalState.functionLLMSettings || {}
117+
)}`
118+
)
119+
return globalState.functionLLMSettings?.[method]?.userPrefText || ''
120+
}
121+
122+
private extractArgumentValues(args: any[]) {
123+
return args.map((a) => (a && typeof a === 'object' && 'value' in a ? a.value : a))
124+
}
125+
50126

51127
async remakeContext(index: number) {
52128
const rootKey = loadRootKeyFromBuffer()

agent-node/src/index.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,61 @@ function connectToManagerWebSocket() {
7171
})
7272

7373
const topicHandler = new RpcTopicHandler(managerInterface, txListener)
74+
75+
// LLM settings extractor from configurations
76+
function applyFnSettingsFromConfigurations(message: any) {
77+
try {
78+
console.log('[INIT] Raw configurations:', JSON.stringify(message?.configurations, null, 2))
79+
} catch {}
80+
if (!message?.configurations) return
81+
globalState.functionLLMSettings = {}
82+
message.configurations.forEach((cfg: any) => {
83+
const act = cfg?.action || {}
84+
if (act.function_name) {
85+
globalState.functionLLMSettings[act.function_name] = {
86+
enabled: !!act.llm_enabled,
87+
userPrefText: act.llm_user_preferences_text || '',
88+
prefs: act.llm_preferences || undefined,
89+
}
90+
}
91+
})
92+
console.log('[INIT] LLM settings for:', Object.keys(globalState.functionLLMSettings))
93+
}
94+
7495
rpcChannel.on('event', (topic, message) => {
96+
// initial payload containing configs
97+
if (topic === 'initial_config') {
98+
if (message.agentConfig?.system_prompt) {
99+
globalState.systemPrompt = message.agentConfig.system_prompt
100+
console.log('[INIT] System prompt loaded')
101+
}
102+
applyFnSettingsFromConfigurations(message)
103+
return
104+
}
105+
106+
// config updates from manager
107+
if (topic === 'config_updated') {
108+
applyFnSettingsFromConfigurations(message)
109+
if (message.agentConfig?.system_prompt) {
110+
globalState.systemPrompt = message.agentConfig.system_prompt
111+
console.log('[INIT] System prompt loaded')
112+
}
113+
}
114+
75115
if (topic == 'instance_count') {
116+
// load system prompt if present
117+
if (message.agentConfig?.system_prompt) {
118+
globalState.systemPrompt = message.agentConfig.system_prompt
119+
console.log('[INIT] System prompt loaded')
120+
}
121+
if (message.config?.system_prompt) {
122+
globalState.systemPrompt = message.config.system_prompt
123+
console.log('Loaded system prompt:', globalState.systemPrompt.slice(0, 80) + '...')
124+
}
125+
126+
// ensure LLM prefs set
127+
applyFnSettingsFromConfigurations(message)
128+
76129
globalRootKeyBuffer.value = message.rootKeyBuffer
77130
globalState.agentName = message.agentName
78131
Array(message.instanceCount)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import "dotenv/config"
2+
import { GoogleGenAI } from "@google/genai";
3+
4+
const ai = new GoogleGenAI({});
5+
6+
export class LLMService {
7+
8+
private apiKey:string
9+
10+
constructor (){
11+
this.apiKey = process.env.GEMINI_API_KEY || ""
12+
if(!this.apiKey){
13+
console.warn("no gemini api key")
14+
}
15+
}
16+
17+
18+
19+
async shouldExecuteFunction(
20+
functionName: string,
21+
functionArgs: any[],
22+
structuredPreferences:any,
23+
userPreferenceText: any ,
24+
systemPrompt:string
25+
26+
27+
):Promise<{
28+
should_execute:boolean,
29+
confidence:number
30+
reasoning:string
31+
}> {
32+
33+
if(!this.apiKey){
34+
35+
console.log("llm not configured")
36+
return {
37+
should_execute: true,
38+
confidence: 0.5,
39+
reasoning: 'LLM not configured, default allow'
40+
}
41+
}
42+
43+
try {
44+
const prompt = this.buildPrompt(functionName, functionArgs, structuredPreferences,
45+
userPreferenceText ,
46+
systemPrompt)
47+
console.log("asking llm")
48+
const response:any = await ai.models.generateContent({
49+
model: "gemini-2.5-flash",
50+
contents: prompt,
51+
});
52+
console.log("response from gemini",response.text);
53+
// todo return repsonse object with parsing
54+
55+
const decision = this.extractJson(response.text)
56+
console.log('after parsing', decision)
57+
return {
58+
should_execute:decision.should_execute,
59+
confidence:decision.confidence,
60+
reasoning:decision.reasoning
61+
}
62+
}
63+
catch (error:any){
64+
console.error("llm failed, the error is this:", error)
65+
return {
66+
should_execute:true,
67+
confidence: 0.6,
68+
reasoning: `LLM service bhayena lol: ${error.message}`
69+
}
70+
}
71+
72+
}
73+
private buildPrompt(functionName: string, functionArgs: any[], structuredPreferences:any,
74+
userPreferenceText: any ,
75+
systemPrompt:string){
76+
const baseSystemP = systemPrompt || "you are an cardano autonomous agent"
77+
const context = structuredPreferences && Object.keys(structuredPreferences).length ? `\nContext:\n${JSON.stringify(structuredPreferences, null, 2)} `: ""
78+
const userPolicy = userPreferenceText ? `\nUser Policy:\n${userPreferenceText}` : ''
79+
console.log("userpolicy is: ", userPolicy)
80+
console.log("context is empty aaile chai: ", context)
81+
82+
return ` ${baseSystemP}
83+
84+
FUNCTION TO EXECUTE: ${functionName}
85+
Args: ${JSON.stringify(functionArgs)}${context}${userPolicy}
86+
87+
Analyze this call strictly against "User Policy".
88+
Return ONLY JSON:
89+
{"should_execute": true/false, "confidence": 0.0-1.0, "reasoning": "brief"}
90+
91+
`
92+
}
93+
94+
95+
96+
97+
98+
99+
100+
extractJson(text: string): any {
101+
// Remove code block markers if present
102+
const cleaned = text.replace(/```json|```/g, '').trim();
103+
// Find the first { and last }
104+
const start = cleaned.indexOf('{');
105+
const end = cleaned.lastIndexOf('}');
106+
if (start !== -1 && end !== -1) {
107+
const jsonString = cleaned.substring(start, end + 1);
108+
try {
109+
return JSON.parse(jsonString);
110+
} catch (e) {
111+
console.error('JSON parse error:', e, jsonString);
112+
}
113+
}
114+
return {
115+
should_execute: false,
116+
confidence: 0.0,
117+
reasoning: 'Failed to parse LLM response'
118+
};
119+
}
120+
}
121+
122+
123+

agent-node/src/utils/agent.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,20 @@ export function saveTxLog(
5656
}
5757
if (mainLog.return) {
5858
txLog.result = mainLog.return
59-
txLog.txHash = mainLog.return.hash
59+
if((mainLog.return as any).blocked_by_llm === true){
60+
txLog.success = false
61+
txLog.message =
62+
(mainLog.return as any).llm_reasoning ||
63+
(mainLog.return as any).message ||
64+
'LLM blocked execution'
65+
} else if ((mainLog.return as any).hash){
66+
txLog.txHash = (mainLog.return as any).hash
67+
txLog.message = (mainLog.return as any).message || 'Function executed successfully'
68+
69+
} else {
70+
txLog.message = (mainLog.return as any).message || 'Function executed successfully'
71+
}
72+
6073
} else if (mainLog.error) {
6174
txLog.result = mainLog.error
6275
txLog.message = mainLog.error && ((mainLog.error as Error).message ?? mainLog.error)
@@ -72,7 +85,20 @@ export function saveTxLog(
7285
}
7386
if (log.return) {
7487
internalLog.result = log.return
75-
internalLog.txHash = log.return.hash
88+
if ((log.return as any).blocked_by_llm === true) {
89+
internalLog.success = false
90+
internalLog.message =
91+
(log.return as any).llm_reasoning ||
92+
(log.return as any).message ||
93+
'LLM blocked execution'
94+
} else if ((log.return as any).hash) {
95+
internalLog.txHash = (log.return as any).hash
96+
internalLog.message = (log.return as any).message || 'Function executed successfully'
97+
} else {
98+
internalLog.message = (log.return as any).message || 'Function executed successfully'
99+
}
100+
101+
76102
} else if (log.error) {
77103
internalLog.result = log.error
78104
internalLog.message = log.error && (log.error.message ?? log.error)

0 commit comments

Comments
 (0)