agentlang 0.9.9 → 0.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/out/extension/main.cjs +38 -38
  2. package/out/extension/main.cjs.map +2 -2
  3. package/out/language/generated/ast.d.ts +1 -1
  4. package/out/language/generated/ast.js +1 -1
  5. package/out/language/generated/grammar.d.ts +1 -1
  6. package/out/language/generated/grammar.js +1 -1
  7. package/out/language/generated/module.d.ts +1 -1
  8. package/out/language/generated/module.js +1 -1
  9. package/out/language/main.cjs +850 -2388
  10. package/out/language/main.cjs.map +4 -4
  11. package/out/runtime/agents/common.d.ts +3 -1
  12. package/out/runtime/agents/common.d.ts.map +1 -1
  13. package/out/runtime/agents/common.js +35 -31
  14. package/out/runtime/agents/common.js.map +1 -1
  15. package/out/runtime/docs.d.ts +1 -0
  16. package/out/runtime/docs.d.ts.map +1 -1
  17. package/out/runtime/docs.js +16 -1
  18. package/out/runtime/docs.js.map +1 -1
  19. package/out/runtime/interpreter.d.ts +1 -0
  20. package/out/runtime/interpreter.d.ts.map +1 -1
  21. package/out/runtime/interpreter.js +60 -9
  22. package/out/runtime/interpreter.js.map +1 -1
  23. package/out/runtime/jsmodules.d.ts +2 -1
  24. package/out/runtime/jsmodules.d.ts.map +1 -1
  25. package/out/runtime/jsmodules.js +2 -1
  26. package/out/runtime/jsmodules.js.map +1 -1
  27. package/out/runtime/loader.d.ts.map +1 -1
  28. package/out/runtime/loader.js +3 -2
  29. package/out/runtime/loader.js.map +1 -1
  30. package/out/runtime/module.d.ts +1 -0
  31. package/out/runtime/module.d.ts.map +1 -1
  32. package/out/runtime/module.js +3 -0
  33. package/out/runtime/module.js.map +1 -1
  34. package/out/runtime/modules/ai.d.ts +12 -0
  35. package/out/runtime/modules/ai.d.ts.map +1 -1
  36. package/out/runtime/modules/ai.js +225 -28
  37. package/out/runtime/modules/ai.js.map +1 -1
  38. package/out/runtime/modules/core.d.ts.map +1 -1
  39. package/out/runtime/modules/core.js +7 -1
  40. package/out/runtime/modules/core.js.map +1 -1
  41. package/out/runtime/resolvers/sqldb/impl.d.ts.map +1 -1
  42. package/out/runtime/resolvers/sqldb/impl.js +37 -6
  43. package/out/runtime/resolvers/sqldb/impl.js.map +1 -1
  44. package/out/runtime/services/documentFetcher.d.ts +70 -0
  45. package/out/runtime/services/documentFetcher.d.ts.map +1 -0
  46. package/out/runtime/services/documentFetcher.js +582 -0
  47. package/out/runtime/services/documentFetcher.js.map +1 -0
  48. package/package.json +2 -1
  49. package/src/language/generated/ast.ts +1 -1
  50. package/src/language/generated/grammar.ts +1 -1
  51. package/src/language/generated/module.ts +1 -1
  52. package/src/runtime/agents/common.ts +37 -31
  53. package/src/runtime/docs.ts +17 -1
  54. package/src/runtime/interpreter.ts +64 -7
  55. package/src/runtime/jsmodules.ts +3 -1
  56. package/src/runtime/loader.ts +3 -2
  57. package/src/runtime/module.ts +4 -0
  58. package/src/runtime/modules/ai.ts +270 -33
  59. package/src/runtime/modules/core.ts +7 -1
  60. package/src/runtime/resolvers/sqldb/impl.ts +36 -6
  61. package/src/runtime/services/documentFetcher.ts +691 -0
@@ -1,7 +1,7 @@
1
1
  import { IfPattern, LiteralPattern } from '../../language/syntax.js';
2
2
  import { trimQuotes } from '../util.js';
3
3
 
4
- export const PlannerInstructions = `Agentlang is a very-high-level declarative language that makes it easy to define business applications as 'models'.
4
+ export const PlannerDataModelInstructions = `Agentlang is a very-high-level declarative language that makes it easy to define business applications as 'models'.
5
5
  The model of a business application consists of entity definitions and workflows defined in "modules".
6
6
  A module is be encoded in a syntax inspired by JavaScript and JSON. Example of a simple module follows:
7
7
 
@@ -29,6 +29,36 @@ record EmailMessage {
29
29
  body String
30
30
  }
31
31
 
32
+ Entities in a module can be connected together in relationships. There are two types of relationships - 'contains' and 'between'.
33
+ 'Contains' relationship is for hierarchical data, as in a Library entity containing Books. 'Between' relationship is for graph-like data,
34
+ like two Profiles in a social media app is connected as friends. A 'between' relationship can be one of the following three types - 'one_one' (one-to-one),
35
+ 'one_many' (one-to-many) and 'many_many' (many-to-many), which is the default.
36
+
37
+ The following example shows how additional profile data for an employee could be defined as a new entity and attached to the Employee entity as a between-relationship:
38
+
39
+ entity Profile {
40
+ id UUID @id @default(uuid()),
41
+ address String @optional,
42
+ photo URL @optional,
43
+ dateOfBirth DateTime @optional
44
+ }
45
+
46
+ relationship EmployeeProfile between (Erp/Employee, Erp/Profile) @one_one
47
+
48
+ The '@one_one' annotation means exactly one Employee and Profile can be related to each other via 'EmployeeProfile'.
49
+
50
+ As an example of 'contains' relaionships, consider modelling task-assignments for an Employee as folllows:
51
+
52
+ entity TaskAssignment {
53
+ id UUID @id @default(uuid()),
54
+ description String,
55
+ assignmentDate DateTime @default(now())
56
+ }
57
+
58
+ relationship EmployeeTaskAssignment contains (Erp/Employee, Erp/TaskAssignment)
59
+ `;
60
+
61
+ export const PlannerWorkflowInstructions = `
32
62
  Another major construct in Agentlang is the 'workflow'. Workflows contains JSON "patterns" that perform CRUD operations on entities.
33
63
  For example, here's is a workflow that creates a new instance of the Employee entity:
34
64
 
@@ -179,25 +209,7 @@ A pattern may execute asynchronously and its eventual result can be handled by p
179
209
  If you are instructed that a particular event will be called asynchronously, always provide the patterns that follows in its '@then' clause. You must add the
180
210
  '@then' clause only if an event's documentation or instruction explicitly requires to do so.
181
211
 
182
- Entities in a module can be connected together in relationships. There are two types of relationships - 'contains' and 'between'.
183
- 'Contains' relationship is for hierarchical data, as in a Library entity containing Books. 'Between' relationship is for graph-like data,
184
- like two Profiles in a social media app is connected as friends. A 'between' relationship can be one of the following three types - 'one_one' (one-to-one),
185
- 'one_many' (one-to-many) and 'many_many' (many-to-many), which is the default.
186
-
187
- The following example shows how additional profile data for an employee could be defined as a new entity and attached to the Employee entity as a between-relationship:
188
-
189
- entity Profile {
190
- id UUID @id @default(uuid()),
191
- address String @optional,
192
- photo URL @optional,
193
- dateOfBirth DateTime @optional
194
- }
195
-
196
- relationship EmployeeProfile between (Erp/Employee, Erp/Profile) @one_one
197
-
198
- The '@one_one' annotation means exactly one Employee and Profile can be related to each other via 'EmployeeProfile'.
199
-
200
- Here's the 'CreateEmployee' workflow updated to create the Employee with the his/her Profile attached:
212
+ Earlier we discussed teh concept of 'between' relationship. Here's the 'CreateEmployee' workflow updated to create the Employee with the his/her Profile attached:
201
213
 
202
214
  workflow CreateEmployee {
203
215
  {Erp/Employee {firstName CreateEmployee.firstName,
@@ -214,17 +226,7 @@ The following pattern can be user to query an Employee along with his Profile:
214
226
  {Erp/Employee {employeeId? "56392e13-0d9a-42f7-b556-0d7cd9468a24"},
215
227
  Erp/EmployeeProfile {Erp/Profile? {}}}
216
228
 
217
- As an example of 'contains' relaionships, consider modelling task-assignments for an Employee as folllows:
218
-
219
- entity TaskAssignment {
220
- id UUID @id @default(uuid()),
221
- description String,
222
- assignmentDate DateTime @default(now())
223
- }
224
-
225
- relationship EmployeeTaskAssignment contains (Erp/Employee, Erp/TaskAssignment)
226
-
227
- The following workflow shows how to assign a new task to an Employee:
229
+ We also discussed the concept of 'contains' relationship and showed an example of modelling employee-task-assignment using it. The following workflow shows how to assign a new task to an Employee:
228
230
 
229
231
  workflow AssignNewTask {
230
232
  {Erp/Employee {employeeId? AssignNewTask.employeeId},
@@ -264,6 +266,10 @@ As an example, if the user request is "send an email to employee 101 with this m
264
266
  {email {to emp.email, body "please call me as soon as possible"}}]
265
267
 
266
268
  You MUST separate each pattern in the array with a semi-colon (;) and never use a comma (,) for this purpose.
269
+ `;
270
+
271
+ export const PlannerInstructions = `${PlannerDataModelInstructions}
272
+ ${PlannerWorkflowInstructions}
267
273
 
268
274
  Now consider the following module definition and generate appropriate patterns in response to the user instructions. You must return only valid patterns or workflows,
269
275
  no other descriptive text or comments are needed.
@@ -44,7 +44,7 @@ async function getPDFParse() {
44
44
  return PDFParse;
45
45
  }
46
46
 
47
- async function parsePdfBuffer(buffer: Uint8Array): Promise<string> {
47
+ export async function parsePdfBuffer(buffer: Uint8Array): Promise<string> {
48
48
  try {
49
49
  const PDFParseClass = await getPDFParse();
50
50
  const parser = new PDFParseClass({
@@ -166,3 +166,19 @@ async function fetchTextFile(path: string): Promise<string> {
166
166
  registerDocFetcher('http', httpFetcher);
167
167
  registerDocFetcher('https', httpFetcher);
168
168
  registerDocFetcher('file', fetchFile);
169
+
170
+ async function documentServiceFetcher(url: string): Promise<string | undefined> {
171
+ try {
172
+ const { documentFetcher } = await import('./services/documentFetcher.js');
173
+ const result = await documentFetcher.fetchDocument({
174
+ title: 'temp', // Title will be updated by handleDocEvent
175
+ url: url,
176
+ });
177
+ return result?.content;
178
+ } catch (error: any) {
179
+ logger.error(`Failed to fetch from document service ${url}: ${error.message}`);
180
+ return undefined;
181
+ }
182
+ }
183
+
184
+ registerDocFetcher('document-service', documentServiceFetcher);
@@ -87,6 +87,7 @@ import {
87
87
  AgentInstance,
88
88
  findAgentByName,
89
89
  normalizeGeneratedCode,
90
+ saveFlowStepResult,
90
91
  } from './modules/ai.js';
91
92
  import { logger } from './logger.js';
92
93
  import {
@@ -103,7 +104,7 @@ import {
103
104
  flushMonitoringData,
104
105
  triggerTimer,
105
106
  } from './modules/core.js';
106
- import { invokeModuleFn } from './jsmodules.js';
107
+ import { getModuleDef, invokeModuleFn } from './jsmodules.js';
107
108
  import { invokeOpenApiEvent, isOpenApiEventInstance } from './openapi.js';
108
109
  import { fetchDoc } from './docs.js';
109
110
  import { FlowSpec, FlowStep, getAgentFlow } from './agents/flows.js';
@@ -111,6 +112,7 @@ import { isMonitoringEnabled } from './state.js';
111
112
  import { Monitor, MonitorEntry } from './monitor.js';
112
113
  import { detailedDiff } from 'deep-object-diff';
113
114
  import { callMcpTool, mcpClientNameFromToolEvent } from './mcpclient.js';
115
+ import { isNodeEnv } from '../utils/runtime.js';
114
116
 
115
117
  export type Result = any;
116
118
 
@@ -441,6 +443,11 @@ export class Environment extends Instance {
441
443
  }
442
444
  }
443
445
 
446
+ softSuspend(): string {
447
+ this.suspensionId = this.preGeneratedSuspensionId;
448
+ return this.suspensionId;
449
+ }
450
+
444
451
  releaseSuspension(): Environment {
445
452
  this.suspensionId = undefined;
446
453
  this.preGeneratedSuspensionId = crypto.randomUUID();
@@ -1752,7 +1759,39 @@ export function isMcpEventInstance(inst: Instance): boolean {
1752
1759
  }
1753
1760
 
1754
1761
  async function handleDocEvent(inst: Instance, env: Environment): Promise<void> {
1755
- const s = await fetchDoc(inst.lookup('url'));
1762
+ const url = inst.lookup('url');
1763
+ if (typeof url === 'string' && url.startsWith('s3://')) {
1764
+ if (!isNodeEnv) {
1765
+ throw new Error('Document fetching is only available in Node.js environment');
1766
+ }
1767
+ const title = inst.lookup('title');
1768
+ const retrievalConfig = inst.lookup('retrievalConfig');
1769
+ const embeddingConfig = inst.lookup('embeddingConfig');
1770
+ const { documentFetcher } = await import('./services/documentFetcher.js');
1771
+ await documentFetcher.fetchDocument({
1772
+ title,
1773
+ url,
1774
+ retrievalConfig,
1775
+ embeddingConfig,
1776
+ });
1777
+ return;
1778
+ }
1779
+
1780
+ if (typeof url === 'string' && url.startsWith('document-service://')) {
1781
+ const title = inst.lookup('title');
1782
+ const retrievalConfig = inst.lookup('retrievalConfig');
1783
+ const embeddingConfig = inst.lookup('embeddingConfig');
1784
+ const { documentFetcher } = await import('./services/documentFetcher.js');
1785
+ await documentFetcher.fetchDocument({
1786
+ title,
1787
+ url,
1788
+ retrievalConfig,
1789
+ embeddingConfig,
1790
+ });
1791
+ return;
1792
+ }
1793
+
1794
+ const s = await fetchDoc(url);
1756
1795
  if (s) {
1757
1796
  const title = inst.lookup('title');
1758
1797
  const doc = makeInstance(
@@ -1998,7 +2037,7 @@ async function agentInvoke(agent: AgentInstance, msg: string, env: Environment):
1998
2037
  isWf = true;
1999
2038
  }
2000
2039
  if (isWf) {
2001
- const wf = await parseWorkflow(rs);
2040
+ const wf = await parseWorkflow(normalizeGeneratedCode(rs));
2002
2041
  if (stmtsExec) {
2003
2042
  await stmtsExec(wf.statements, env);
2004
2043
  } else {
@@ -2010,7 +2049,9 @@ async function agentInvoke(agent: AgentInstance, msg: string, env: Environment):
2010
2049
  const r = await stmtsExec([stmt], env);
2011
2050
  env.setLastResult(r);
2012
2051
  } else {
2013
- env.setLastResult(await parseAndEvaluateStatement(rs, undefined, env));
2052
+ env.setLastResult(
2053
+ await parseAndEvaluateStatement(normalizeGeneratedCode(rs), undefined, env)
2054
+ );
2014
2055
  }
2015
2056
  }
2016
2057
  agent.maybeAddScratchData(env);
@@ -2139,7 +2180,7 @@ export async function restartFlow(
2139
2180
  const rootAgent: AgentInstance = await findAgentByName(agentName, env);
2140
2181
  const flow = getAgentFlow(agentName, rootAgent.moduleName);
2141
2182
  if (flow) {
2142
- const newCtx = `${ctx}\n${step} --> ${userData}\n`;
2183
+ const newCtx = `${ctx}\nRestart the flow at ${step} using the following user-input as additional guidance:\n${userData}\n`;
2143
2184
  env.setScratchPad(JSON.parse(spad));
2144
2185
  await iterateOnFlow(flow, rootAgent, newCtx, env);
2145
2186
  }
@@ -2167,7 +2208,8 @@ async function iterateOnFlow(
2167
2208
  let needAgentProcessing = preprocResult.needAgentProcessing;
2168
2209
  let context = initContext;
2169
2210
  let stepc = 0;
2170
- const iterId = crypto.randomUUID();
2211
+ const chatId = env.getActiveEventInstance()?.lookup('chatId');
2212
+ const iterId = chatId || crypto.randomUUID();
2171
2213
  console.debug(`Starting iteration ${iterId} on flow: ${flow}`);
2172
2214
  const executedSteps = new Set<string>();
2173
2215
  const monitoringEnabled = isMonitoringEnabled();
@@ -2215,6 +2257,12 @@ async function iterateOnFlow(
2215
2257
  `\n----> Completed execution of step ${step}, iteration id ${iterId} with result:\n${rs}`
2216
2258
  );
2217
2259
  context = `${context}\n${step} --> ${rs}\n`;
2260
+ if (chatId) {
2261
+ const suspEnv = new Environment(env.name, env);
2262
+ suspEnv.softSuspend();
2263
+ await saveFlowSuspension(rootAgent, context, step, suspEnv);
2264
+ await saveFlowStepResult(chatId, step, rs, suspEnv.getSuspensionId(), env);
2265
+ }
2218
2266
  if (isfxc) {
2219
2267
  preprocResult = await preprocessStep(rs, rootModuleName, env);
2220
2268
  } else {
@@ -2462,10 +2510,19 @@ async function followReference(env: Environment, s: string): Promise<Result> {
2462
2510
  for (let i = 0; i < refs.length; ++i) {
2463
2511
  const r: string = refs[i];
2464
2512
  const v: Result | undefined = await getRef(r, src, env);
2465
- if (v === undefined) return EmptyResult;
2513
+ if (v === undefined || v === null) {
2514
+ result = EmptyResult;
2515
+ break;
2516
+ }
2466
2517
  result = v;
2467
2518
  src = result;
2468
2519
  }
2520
+ if (result === EmptyResult) {
2521
+ result = getModuleDef(s);
2522
+ if (result === undefined) {
2523
+ result = EmptyResult;
2524
+ }
2525
+ }
2469
2526
  return result;
2470
2527
  }
2471
2528
 
@@ -303,7 +303,7 @@ export async function invokeModuleFn(
303
303
  }
304
304
  }
305
305
 
306
- export function getModuleFn(fqFnName: string): Function | undefined {
306
+ export function getModuleDef(fqFnName: string): any {
307
307
  const refs: string[] = splitRefs(fqFnName);
308
308
  const m = importedModules.get(refs[0]);
309
309
  if (m !== undefined) {
@@ -311,4 +311,6 @@ export function getModuleFn(fqFnName: string): Function | undefined {
311
311
  } else return undefined;
312
312
  }
313
313
 
314
+ export const getModuleFn = getModuleDef;
315
+
314
316
  setModuleFnFetcher(getModuleFn);
@@ -819,7 +819,8 @@ async function addAgentDefinition(
819
819
  }
820
820
  const ov = v;
821
821
  if (apdef.value.id || apdef.value.ref || apdef.value.array) {
822
- v = `"${v}"`;
822
+ if (!(apdef.name === 'instruction' || apdef.name === 'role' || apdef.name === 'llm'))
823
+ v = `"${v}"`;
823
824
  } else if (apdef.value.str) {
824
825
  v = `"${escapeSpecialChars(v)}"`;
825
826
  }
@@ -837,7 +838,7 @@ async function addAgentDefinition(
837
838
  // Create a copy of attrsStrs for the database operation
838
839
  const dbAttrsStrs = [...attrsStrs];
839
840
  // Only add llm to database attributes if we have one
840
- if (llmName) {
841
+ if (llmName && createDefaultLLM) {
841
842
  dbAttrsStrs.push(`llm "${llmName}"`);
842
843
  }
843
844
 
@@ -3731,6 +3731,10 @@ export function newInstanceAttributes(): InstanceAttributes {
3731
3731
  return new Map<string, any>();
3732
3732
  }
3733
3733
 
3734
+ export function objectToInstanceAttributes(obj: any): InstanceAttributes {
3735
+ return new Map(Object.entries(obj));
3736
+ }
3737
+
3734
3738
  const EmptyInstanceAttributes: InstanceAttributes = newInstanceAttributes();
3735
3739
 
3736
3740
  export class Instance {
@@ -1,4 +1,5 @@
1
1
  import {
2
+ DefaultModuleName,
2
3
  escapeSpecialChars,
3
4
  isFqName,
4
5
  isString,
@@ -29,9 +30,10 @@ import {
29
30
  isModule,
30
31
  makeInstance,
31
32
  newInstanceAttributes,
33
+ objectToInstanceAttributes,
32
34
  Record,
33
- Retry,
34
35
  resolveDocumentAliases,
36
+ Retry,
35
37
  } from '../module.js';
36
38
  import { provider } from '../agents/registry.js';
37
39
  import {
@@ -66,6 +68,8 @@ import { FlowStep } from '../agents/flows.js';
66
68
  import Handlebars from 'handlebars';
67
69
  import { Statement } from '../../language/generated/ast.js';
68
70
  import { isMonitoringEnabled, TtlCache } from '../state.js';
71
+ import { isNodeEnv } from '../../utils/runtime.js';
72
+ import { getFileSystem } from '../../utils/fs-utils.js';
69
73
 
70
74
  export const CoreAIModuleName = makeCoreModuleName('ai');
71
75
  export const AgentEntityName = 'Agent';
@@ -74,27 +78,6 @@ export const AgentLearnerType = 'learner';
74
78
 
75
79
  const AgentEvalType = 'eval';
76
80
 
77
- function buildEmbeddingConfig(): object {
78
- const config: any = {
79
- provider: process.env.AGENTLANG_EMBEDDING_PROVIDER || 'openai',
80
- model: process.env.AGENTLANG_EMBEDDING_MODEL || 'text-embedding-3-small',
81
- chunkSize: 1000,
82
- chunkOverlap: 200,
83
- };
84
-
85
- if (process.env.AGENTLANG_EMBEDDING_CHUNKSIZE) {
86
- config.chunkSize = parseInt(process.env.AGENTLANG_EMBEDDING_CHUNKSIZE, 1000);
87
- }
88
-
89
- if (process.env.AGENTLANG_EMBEDDING_CHUNKOVERLAP) {
90
- config.chunkOverlap = parseInt(process.env.AGENTLANG_EMBEDDING_CHUNKOVERLAP, 200);
91
- }
92
-
93
- return config;
94
- }
95
-
96
- const embeddingConfig = JSON.stringify(buildEmbeddingConfig());
97
-
98
81
  export default `module ${CoreAIModuleName}
99
82
 
100
83
  import "./modules/ai.js" @as ai
@@ -139,12 +122,24 @@ workflow saveAgentChatSession {
139
122
  entity Document {
140
123
  title String @id,
141
124
  content String,
142
- @meta {"fullTextSearch": "*", "embeddingConfig": ${embeddingConfig}}
125
+ embeddingConfig Map @optional,
126
+ @meta {"fullTextSearch": "*"}
143
127
  }
144
128
 
145
129
  event doc {
146
130
  title String,
147
- url String
131
+ url String,
132
+ config Map @optional,
133
+ retrievalConfig Map @optional,
134
+ embeddingConfig Map @optional
135
+ }
136
+
137
+ workflow processDoc {
138
+ // Fetch document from URL and create Document entity
139
+ // Supports: local paths, http/https URLs, s3:// URLs
140
+ // S3 config can be provided via retrievalConfig or env vars
141
+ // Embedding config can customize chunking behavior
142
+ await ai.fetchAndCreateDocument(doc.title, doc.url, doc.retrievalConfig, doc.embeddingConfig)
148
143
  }
149
144
 
150
145
  entity Directive {
@@ -193,6 +188,27 @@ entity AgentLearningResult {
193
188
  workflow agentLearning {
194
189
  await ai.processAgentLearning(agentLearning.agentModuleName, agentLearning.agentName, agentLearning.instruction)
195
190
  }
191
+
192
+ entity AgentFlowStep {
193
+ id UUID @id @default(uuid()),
194
+ chatId String @indexed,
195
+ step String @indexed,
196
+ result String,
197
+ suspensionId String
198
+ }
199
+
200
+ @public event restartFlow {
201
+ chatId String,
202
+ step String,
203
+ userInput String
204
+ }
205
+
206
+ workflow restartFlow {
207
+ await ai.loadFlowStep(restartFlow.chatId, restartFlow.step) @as fs;
208
+ if (fs) {
209
+ {${DefaultModuleName}/restartSuspension {id fs.suspensionId, data restartFlow.userInput}}
210
+ }
211
+ }
196
212
  `;
197
213
 
198
214
  enum AgentCacheType {
@@ -622,7 +638,7 @@ export class AgentInstance {
622
638
  Only return a pure JSON object with no extra text, annotations etc.`;
623
639
  }
624
640
  const spad = env.getScratchPad();
625
- if (spad !== undefined) {
641
+ if (spad !== undefined && Object.keys(spad).length > 0) {
626
642
  if (finalInstruction.indexOf('{{') > 0) {
627
643
  return AgentInstance.maybeRewriteTemplatePatterns(spad, finalInstruction, env);
628
644
  } else {
@@ -1282,17 +1298,38 @@ function processScenarioResponse(resp: string): string {
1282
1298
  return resp;
1283
1299
  }
1284
1300
 
1301
+ type ExtractedCode = {
1302
+ language: string | null;
1303
+ code: string;
1304
+ };
1305
+
1306
+ export function extractFencedCodeBlocks(markdown: string): ExtractedCode[] {
1307
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
1308
+ const blocks: ExtractedCode[] = [];
1309
+ let match;
1310
+
1311
+ while ((match = codeBlockRegex.exec(markdown)) !== null) {
1312
+ blocks.push({
1313
+ language: match[1] || null,
1314
+ code: match[2],
1315
+ });
1316
+ }
1317
+
1318
+ return blocks;
1319
+ }
1320
+
1285
1321
  export function normalizeGeneratedCode(code: string | undefined): string {
1286
1322
  if (code !== undefined) {
1287
- let s = code.trim();
1288
- if (s.startsWith('```')) {
1289
- const idx = s.indexOf('\n');
1290
- s = s.substring(idx).trimStart();
1291
- }
1292
- if (s.endsWith('```')) {
1293
- s = s.substring(0, s.length - 3);
1323
+ const blocks = extractFencedCodeBlocks(code);
1324
+ if (blocks.length > 0) {
1325
+ return blocks
1326
+ .map((v: ExtractedCode) => {
1327
+ return v.code;
1328
+ })
1329
+ .join('\n\n');
1330
+ } else {
1331
+ return code;
1294
1332
  }
1295
- return s;
1296
1333
  } else {
1297
1334
  return '';
1298
1335
  }
@@ -1392,3 +1429,203 @@ export async function processAgentLearning(
1392
1429
  await parseAndInternAgentLearning(moduleName, agentName, learning, env);
1393
1430
  return { agentLearning: { result: learning } };
1394
1431
  }
1432
+
1433
+ const LocalAgentgFlow = true;
1434
+ const LocalFlowStepsRootDirName = 'flows';
1435
+ const AgentFlowStep = 'AgentFlowStep';
1436
+
1437
+ export async function saveFlowStepResultLocally(
1438
+ chatId: string,
1439
+ step: string,
1440
+ result: string,
1441
+ suspensionId: string
1442
+ ): Promise<Instance | undefined> {
1443
+ const fs = await getFileSystem();
1444
+ const attrs = newInstanceAttributes()
1445
+ .set('chatId', chatId)
1446
+ .set('step', step)
1447
+ .set('result', result)
1448
+ .set('suspensionId', suspensionId);
1449
+ const inst = makeInstance(CoreAIModuleName, AgentFlowStep, attrs);
1450
+ const rootDirName = LocalFlowStepsRootDirName;
1451
+ if (!(await fs.exists(rootDirName))) {
1452
+ await fs.mkdir(rootDirName);
1453
+ }
1454
+ const dirName = `${rootDirName}/${chatId}`;
1455
+ if (!(await fs.exists(dirName))) {
1456
+ await fs.mkdir(dirName);
1457
+ }
1458
+ const fileName = `${dirName}/${step}`;
1459
+ if (await fs.exists(fileName)) {
1460
+ await fs.unlink(fileName);
1461
+ }
1462
+ await fs.writeFile(fileName, JSON.stringify(inst.attributesAsObject(true)));
1463
+ return inst;
1464
+ }
1465
+
1466
+ export async function saveFlowStepResult(
1467
+ chatId: string,
1468
+ step: string,
1469
+ result: string,
1470
+ suspensionId: string,
1471
+ env: Environment
1472
+ ): Promise<Instance | undefined> {
1473
+ if (LocalAgentgFlow) {
1474
+ return await saveFlowStepResultLocally(chatId, step, result, suspensionId);
1475
+ } else {
1476
+ const t = `${CoreAIModuleName}/${AgentFlowStep}`;
1477
+ try {
1478
+ await parseAndEvaluateStatement(
1479
+ `purge {${t} {chatId? "${chatId}", step? "${step}"}}`,
1480
+ undefined,
1481
+ env
1482
+ );
1483
+ const inst: Instance = await parseAndEvaluateStatement(
1484
+ `{${t} {chatId "${chatId}", step "${step}", result "${escapeSpecialChars(result)}", suspensionId "${suspensionId}"}}`,
1485
+ undefined,
1486
+ env
1487
+ );
1488
+ if (isInstanceOfType(inst, t)) return inst;
1489
+ else return undefined;
1490
+ } catch (reason: any) {
1491
+ logger.error(`failed to save flow result for step ${step} - ${reason}`);
1492
+ return undefined;
1493
+ }
1494
+ }
1495
+ }
1496
+
1497
+ export async function loadLocalFlowStepResults(chatId: string): Promise<Instance[]> {
1498
+ const fs = await getFileSystem();
1499
+ const dirName = `${LocalFlowStepsRootDirName}/${chatId}`;
1500
+ if (await fs.exists(dirName)) {
1501
+ const fileNames = await fs.readdir(dirName);
1502
+ const result: Instance[] = new Array<Instance>();
1503
+ for (let i = 0; i < fileNames.length; ++i) {
1504
+ const fileName = fileNames[i];
1505
+ const attrs = objectToInstanceAttributes(
1506
+ JSON.parse(await fs.readFile(`${dirName}/${fileName}`))
1507
+ );
1508
+ result.push(makeInstance(CoreAIModuleName, AgentFlowStep, attrs));
1509
+ }
1510
+ return result;
1511
+ } else {
1512
+ return [];
1513
+ }
1514
+ }
1515
+
1516
+ export async function loadFlowStepResults(chatId: string): Promise<Instance[]> {
1517
+ if (LocalAgentgFlow) {
1518
+ return await loadLocalFlowStepResults(chatId);
1519
+ } else {
1520
+ try {
1521
+ return await parseAndEvaluateStatement(`{${CoreAIModuleName}/${AgentFlowStep} {
1522
+ chatId? "${chatId}"}}`);
1523
+ } catch (reason: any) {
1524
+ logger.error(`failed to query flow-steps for ${chatId} - ${reason}`);
1525
+ return [];
1526
+ }
1527
+ }
1528
+ }
1529
+
1530
+ export async function loadLocalFlowStep(
1531
+ chatId: string,
1532
+ step: string
1533
+ ): Promise<Instance | undefined> {
1534
+ const fs = await getFileSystem();
1535
+ const dirName = `${LocalFlowStepsRootDirName}/${chatId}`;
1536
+ if (await fs.exists(dirName)) {
1537
+ const fileNames = await fs.readdir(dirName);
1538
+ for (let i = 0; i < fileNames.length; ++i) {
1539
+ const fileName = fileNames[i];
1540
+ if (fileName === step) {
1541
+ const attrs = objectToInstanceAttributes(
1542
+ JSON.parse(await fs.readFile(`${dirName}/${fileName}`))
1543
+ );
1544
+ return makeInstance(CoreAIModuleName, AgentFlowStep, attrs);
1545
+ }
1546
+ }
1547
+ return undefined;
1548
+ } else {
1549
+ return undefined;
1550
+ }
1551
+ }
1552
+
1553
+ export async function loadFlowStep(chatId: string, step: string): Promise<Instance | undefined> {
1554
+ if (LocalAgentgFlow) {
1555
+ return await loadLocalFlowStep(chatId, step);
1556
+ } else {
1557
+ try {
1558
+ const insts: Instance[] =
1559
+ await parseAndEvaluateStatement(`{${CoreAIModuleName}/${AgentFlowStep} {
1560
+ chatId? "${chatId}", step? "${step}"}}`);
1561
+ if (insts.length > 0) {
1562
+ return insts[0];
1563
+ } else {
1564
+ return undefined;
1565
+ }
1566
+ } catch (reason: any) {
1567
+ logger.error(`failed to lookup step ${step} for flow ${chatId} - ${reason}`);
1568
+ return undefined;
1569
+ }
1570
+ }
1571
+ }
1572
+
1573
+ export async function fetchAndCreateDocument(
1574
+ title: string,
1575
+ url: string,
1576
+ retrievalConfig: Map<string, any> | undefined,
1577
+ embeddingConfig: Map<string, any> | undefined,
1578
+ _env: Environment
1579
+ ): Promise<any> {
1580
+ if (!isNodeEnv) {
1581
+ throw new Error('Document fetching is only available in Node.js environment');
1582
+ }
1583
+
1584
+ const { documentFetcher } = await import('../services/documentFetcher.js');
1585
+
1586
+ const config: any = {
1587
+ title,
1588
+ url,
1589
+ };
1590
+
1591
+ if (retrievalConfig) {
1592
+ const provider = retrievalConfig.get('provider');
1593
+ const providerConfig = retrievalConfig.get('config');
1594
+
1595
+ config.retrievalConfig = {
1596
+ provider: provider || 's3',
1597
+ config: providerConfig
1598
+ ? {
1599
+ region: providerConfig.get('region'),
1600
+ endpoint: providerConfig.get('endpoint'),
1601
+ accessKeyId: providerConfig.get('accessKeyId'),
1602
+ secretAccessKey: providerConfig.get('secretAccessKey'),
1603
+ forcePathStyle: providerConfig.get('forcePathStyle'),
1604
+ }
1605
+ : {},
1606
+ };
1607
+ }
1608
+
1609
+ if (embeddingConfig) {
1610
+ config.embeddingConfig = {
1611
+ provider: embeddingConfig.get('provider'),
1612
+ model: embeddingConfig.get('model'),
1613
+ chunkSize: embeddingConfig.get('chunkSize'),
1614
+ chunkOverlap: embeddingConfig.get('chunkOverlap'),
1615
+ };
1616
+ }
1617
+
1618
+ const document = await documentFetcher.fetchDocument(config);
1619
+
1620
+ if (document) {
1621
+ return {
1622
+ document: {
1623
+ title: document.title,
1624
+ url: document.url,
1625
+ format: document.format,
1626
+ },
1627
+ };
1628
+ } else {
1629
+ throw new Error(`Failed to fetch document: ${title} from ${url}`);
1630
+ }
1631
+ }