dirac-lang 0.1.14 → 0.1.16
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.
- package/config.test.yml +5 -0
- package/dist/{chunk-VOD5GGHB.js → chunk-CP4YCRBG.js} +233 -146
- package/dist/{chunk-SOHSXT3M.js → chunk-FEOBVBSI.js} +4 -2
- package/dist/chunk-HRHAMPOB.js +123 -0
- package/dist/cli.js +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -3
- package/dist/{interpreter-FT22JGOO.js → interpreter-GNO4J3HN.js} +2 -1
- package/dist/parser-T6W3WPLU.js +6 -0
- package/dist/{tag-validator-DYWFTEBE.js → tag-validator-CLYGULLQ.js} +10 -8
- package/dist/test-runner.js +6 -5
- package/examples/llm-command-more.di +6 -0
- package/package.json +1 -1
- package/src/runtime/interpreter.ts +10 -0
- package/src/tags/attr.ts +64 -0
- package/src/tags/available-subroutines.ts +19 -6
- package/src/tags/call.ts +4 -0
- package/src/tags/defvar.ts +53 -2
- package/src/tags/foreach.ts +170 -0
- package/src/tags/tag-check.ts +19 -6
- package/src/test-runner.ts +4 -4
- package/src/types/index.ts +1 -0
- package/src/utils/tag-validator.ts +12 -8
- package/tests/available-subroutines-foreach.test.di +32 -0
- package/tests/tag-check.test.di +27 -0
- package/config.yml +0 -9
- package/config.yml.bak +0 -8
- package/config.yml.ollama +0 -6
- package/config.yml.openai +0 -6
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,11 +2,13 @@ import {
|
|
|
2
2
|
createLLMAdapter,
|
|
3
3
|
execute,
|
|
4
4
|
executeUserCommand
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-FEOBVBSI.js";
|
|
6
6
|
import {
|
|
7
|
-
DiracParser,
|
|
8
7
|
integrate
|
|
9
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-CP4YCRBG.js";
|
|
9
|
+
import {
|
|
10
|
+
DiracParser
|
|
11
|
+
} from "./chunk-HRHAMPOB.js";
|
|
10
12
|
import "./chunk-E7PWEMZA.js";
|
|
11
13
|
import {
|
|
12
14
|
createSession,
|
|
@@ -4,21 +4,23 @@ import yaml from "js-yaml";
|
|
|
4
4
|
var SIMILARITY_CUTOFF = 0.75;
|
|
5
5
|
function getEmbeddingServerConfig() {
|
|
6
6
|
try {
|
|
7
|
-
const
|
|
7
|
+
const configPath = process.env.DIRAC_CONFIG || "config.yml";
|
|
8
|
+
const config = yaml.load(fs.readFileSync(configPath, "utf8"));
|
|
8
9
|
const host = config.embeddingServer?.host || "localhost";
|
|
9
|
-
const port = config.embeddingServer?.port ||
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const port = config.embeddingServer?.port || 11434;
|
|
11
|
+
const model = config.embeddingServer?.model || "embeddinggemma";
|
|
12
|
+
return { host, port, model };
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return { host: "localhost", port: 11434, model: "embeddinggemma" };
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
async function getEmbeddings(tags) {
|
|
16
|
-
const { host, port } = getEmbeddingServerConfig();
|
|
18
|
+
const { host, port, model } = getEmbeddingServerConfig();
|
|
17
19
|
return await Promise.all(tags.map(async (tag) => {
|
|
18
20
|
const response = await fetch(`http://${host}:${port}/api/embeddings`, {
|
|
19
21
|
method: "POST",
|
|
20
22
|
headers: { "Content-Type": "application/json" },
|
|
21
|
-
body: JSON.stringify({ model
|
|
23
|
+
body: JSON.stringify({ model, prompt: tag })
|
|
22
24
|
});
|
|
23
25
|
const data = await response.json();
|
|
24
26
|
return data.embedding;
|
|
@@ -111,7 +113,7 @@ async function validateDiracCode(session, ast, options = {}) {
|
|
|
111
113
|
const results = [];
|
|
112
114
|
const errorMessages = [];
|
|
113
115
|
async function validateElement(element) {
|
|
114
|
-
if (element.tag && element.tag !== "dirac" && element.tag !== "") {
|
|
116
|
+
if (element.tag && element.tag !== "dirac" && element.tag.trim() !== "") {
|
|
115
117
|
const result = await validateTag(session, element, options);
|
|
116
118
|
results.push(result);
|
|
117
119
|
if (!result.valid) {
|
package/dist/test-runner.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
DiracParser,
|
|
3
2
|
integrate
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-CP4YCRBG.js";
|
|
4
|
+
import {
|
|
5
|
+
DiracParser
|
|
6
|
+
} from "./chunk-HRHAMPOB.js";
|
|
5
7
|
import "./chunk-E7PWEMZA.js";
|
|
6
8
|
import {
|
|
7
9
|
createSession,
|
|
@@ -17,9 +19,8 @@ var TestRunner = class {
|
|
|
17
19
|
config;
|
|
18
20
|
constructor(testDir = "tests", configPath) {
|
|
19
21
|
this.testDir = testDir;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.config = yaml.load(fs.readFileSync(defaultConfigPath, "utf8")) || {};
|
|
22
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
23
|
+
this.config = yaml.load(fs.readFileSync(configPath, "utf8")) || {};
|
|
23
24
|
} else {
|
|
24
25
|
this.config = { llmProvider: "ollama", llmModel: "llama2" };
|
|
25
26
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<dirac>
|
|
2
|
+
<subroutine name="background" description="set the background color." param-color="string:required:color name" >
|
|
3
|
+
this is my color: <variable name="color" />
|
|
4
|
+
</subroutine>
|
|
5
|
+
<llm execute="true">what is the color of red mix with yellow and set the background color to that color</llm>
|
|
6
|
+
</dirac>
|
package/package.json
CHANGED
|
@@ -27,6 +27,8 @@ import { executeCatch } from '../tags/catch.js';
|
|
|
27
27
|
import { executeException } from '../tags/exception.js';
|
|
28
28
|
import { executeTestIf } from '../tags/test-if.js';
|
|
29
29
|
import { executeAvailableSubroutines } from '../tags/available-subroutines.js';
|
|
30
|
+
import { executeForeach } from '../tags/foreach.js';
|
|
31
|
+
import { executeAttr } from '../tags/attr.js';
|
|
30
32
|
|
|
31
33
|
export async function integrate(session: DiracSession, element: DiracElement): Promise<void> {
|
|
32
34
|
// Check execution limits
|
|
@@ -142,6 +144,14 @@ export async function integrate(session: DiracSession, element: DiracElement): P
|
|
|
142
144
|
case 'available-subroutines':
|
|
143
145
|
await executeAvailableSubroutines(session, element);
|
|
144
146
|
break;
|
|
147
|
+
|
|
148
|
+
case 'foreach':
|
|
149
|
+
await executeForeach(session, element);
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case 'attr':
|
|
153
|
+
await executeAttr(session, element);
|
|
154
|
+
break;
|
|
145
155
|
|
|
146
156
|
case 'require_module':
|
|
147
157
|
await executeRequireModule(session, element);
|
package/src/tags/attr.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <attr> tag - extract attribute value from XML item
|
|
3
|
+
* Usage: <attr name="description" from="$sub" />
|
|
4
|
+
* or: <attr name="param-format" from="$sub" />
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DiracSession, DiracElement } from '../types/index.js';
|
|
8
|
+
import { getVariable, substituteVariables } from '../runtime/session.js';
|
|
9
|
+
|
|
10
|
+
export async function executeAttr(session: DiracSession, element: DiracElement): Promise<void> {
|
|
11
|
+
const nameAttr = element.attributes.name;
|
|
12
|
+
const fromAttr = element.attributes.from;
|
|
13
|
+
|
|
14
|
+
if (!nameAttr) {
|
|
15
|
+
throw new Error('<attr> requires "name" attribute');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!fromAttr) {
|
|
19
|
+
throw new Error('<attr> requires "from" attribute');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Get the XML item from variable
|
|
23
|
+
let item: any;
|
|
24
|
+
|
|
25
|
+
if (fromAttr.startsWith('$')) {
|
|
26
|
+
const varName = fromAttr.substring(1);
|
|
27
|
+
item = getVariable(session, varName);
|
|
28
|
+
} else {
|
|
29
|
+
// Try substitution
|
|
30
|
+
const substituted = substituteVariables(session, fromAttr);
|
|
31
|
+
item = getVariable(session, substituted);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!item) {
|
|
35
|
+
// No output if item not found
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Extract the attribute value
|
|
40
|
+
let value = '';
|
|
41
|
+
|
|
42
|
+
if (typeof item === 'object') {
|
|
43
|
+
// If it's a structured XML item (from foreach)
|
|
44
|
+
if (item.attributes && typeof item.attributes === 'object') {
|
|
45
|
+
value = item.attributes[nameAttr] || '';
|
|
46
|
+
}
|
|
47
|
+
// If it's a direct object with the attribute
|
|
48
|
+
else if (item[nameAttr] !== undefined) {
|
|
49
|
+
value = String(item[nameAttr]);
|
|
50
|
+
}
|
|
51
|
+
} else if (typeof item === 'string') {
|
|
52
|
+
// If it's a string, try to parse as XML and extract
|
|
53
|
+
value = extractAttrFromXml(item, nameAttr);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
session.output.push(value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractAttrFromXml(xml: string, attrName: string): string {
|
|
60
|
+
// Simple regex-based extraction for single-element XML
|
|
61
|
+
const regex = new RegExp(`${attrName}="([^"]*)"`, 'i');
|
|
62
|
+
const match = xml.match(regex);
|
|
63
|
+
return match ? match[1] : '';
|
|
64
|
+
}
|
|
@@ -12,20 +12,32 @@ export async function executeAvailableSubroutines(
|
|
|
12
12
|
// Get all subroutines from current boundary to top of stack
|
|
13
13
|
const availableSubroutines = new Map<string, DiracElement>();
|
|
14
14
|
|
|
15
|
+
// Get the name of the currently executing subroutine from the boundary
|
|
16
|
+
const currentSubroutineName = session.subBoundary < session.subroutines.length
|
|
17
|
+
? session.subroutines[session.subBoundary].name
|
|
18
|
+
: null;
|
|
19
|
+
|
|
15
20
|
// Read from top of stack (most recent) backwards to boundary
|
|
16
21
|
// This ensures we get the latest definition (handles extends override)
|
|
17
22
|
for (let i = session.subroutines.length - 1; i >= session.subBoundary; i--) {
|
|
18
23
|
const sub = session.subroutines[i];
|
|
19
24
|
|
|
25
|
+
// Skip the currently executing subroutine itself
|
|
26
|
+
if (sub.name === currentSubroutineName) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
// Only add if not already seen (first occurrence wins)
|
|
21
31
|
if (!availableSubroutines.has(sub.name)) {
|
|
22
32
|
availableSubroutines.set(sub.name, sub.element);
|
|
23
33
|
}
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
// Generate output
|
|
36
|
+
// Generate structured output with container
|
|
37
|
+
session.output.push('<subroutines>');
|
|
38
|
+
|
|
27
39
|
for (const [name, subElement] of availableSubroutines) {
|
|
28
|
-
const attrs: string[] = [];
|
|
40
|
+
const attrs: string[] = [`name="${escapeXml(name)}"`];
|
|
29
41
|
|
|
30
42
|
// Add description if available
|
|
31
43
|
const description = subElement.attributes.description;
|
|
@@ -36,15 +48,16 @@ export async function executeAvailableSubroutines(
|
|
|
36
48
|
// Add all param-* attributes with their definitions
|
|
37
49
|
for (const [attrName, attrValue] of Object.entries(subElement.attributes)) {
|
|
38
50
|
if (attrName.startsWith('param-')) {
|
|
39
|
-
|
|
40
|
-
attrs.push(`${paramName}="${escapeXml(attrValue)}"`);
|
|
51
|
+
attrs.push(`${attrName}="${escapeXml(attrValue)}"`);
|
|
41
52
|
}
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
// Build the output tag
|
|
45
|
-
const attrString = attrs.
|
|
46
|
-
session.output.push(
|
|
56
|
+
const attrString = attrs.join(' ');
|
|
57
|
+
session.output.push(` <subroutine ${attrString} />`);
|
|
47
58
|
}
|
|
59
|
+
|
|
60
|
+
session.output.push('</subroutines>');
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
function escapeXml(text: string): string {
|
package/src/tags/call.ts
CHANGED
|
@@ -135,6 +135,10 @@ async function executeCallInternal(
|
|
|
135
135
|
const wasReturn = session.isReturn;
|
|
136
136
|
session.isReturn = false;
|
|
137
137
|
|
|
138
|
+
// Track current subroutine name for available-subroutines
|
|
139
|
+
const oldSubroutineName = session.currentSubroutineName;
|
|
140
|
+
session.currentSubroutineName = callElement.tag;
|
|
141
|
+
|
|
138
142
|
// For extend execution, skip subroutine registration during body execution
|
|
139
143
|
const oldSkipFlag = session.skipSubroutineRegistration;
|
|
140
144
|
if (isExtendExecution) {
|
package/src/tags/defvar.ts
CHANGED
|
@@ -27,8 +27,10 @@ export async function executeDefvar(session: DiracSession, element: DiracElement
|
|
|
27
27
|
if (valueAttr !== undefined) {
|
|
28
28
|
value = substituteVariables(session, valueAttr);
|
|
29
29
|
} else if (literal) {
|
|
30
|
-
// Literal mode:
|
|
31
|
-
if (element.
|
|
30
|
+
// Literal mode: serialize children to XML text without executing
|
|
31
|
+
if (element.children && element.children.length > 0) {
|
|
32
|
+
value = serializeToXml(element.children);
|
|
33
|
+
} else if (element.text) {
|
|
32
34
|
value = substituteVariables(session, element.text);
|
|
33
35
|
} else {
|
|
34
36
|
value = '';
|
|
@@ -60,3 +62,52 @@ export async function executeDefvar(session: DiracSession, element: DiracElement
|
|
|
60
62
|
|
|
61
63
|
setVariable(session, name, value, visible);
|
|
62
64
|
}
|
|
65
|
+
|
|
66
|
+
function serializeToXml(children: DiracElement[]): string {
|
|
67
|
+
let xml = '';
|
|
68
|
+
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
if (!child.tag || child.tag === '') {
|
|
71
|
+
// Text node
|
|
72
|
+
xml += child.text || '';
|
|
73
|
+
} else {
|
|
74
|
+
// Element node
|
|
75
|
+
xml += `<${child.tag}`;
|
|
76
|
+
|
|
77
|
+
// Add attributes
|
|
78
|
+
for (const [attrName, attrValue] of Object.entries(child.attributes)) {
|
|
79
|
+
xml += ` ${attrName}="${escapeXml(attrValue)}"`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Self-closing or with children
|
|
83
|
+
if (child.children.length === 0 && !child.text) {
|
|
84
|
+
xml += ' />';
|
|
85
|
+
} else {
|
|
86
|
+
xml += '>';
|
|
87
|
+
|
|
88
|
+
// Add text content if present
|
|
89
|
+
if (child.text) {
|
|
90
|
+
xml += escapeXml(child.text);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add children
|
|
94
|
+
if (child.children.length > 0) {
|
|
95
|
+
xml += serializeToXml(child.children);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
xml += `</${child.tag}>`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return xml;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function escapeXml(text: string): string {
|
|
107
|
+
return text
|
|
108
|
+
.replace(/&/g, '&')
|
|
109
|
+
.replace(/</g, '<')
|
|
110
|
+
.replace(/>/g, '>')
|
|
111
|
+
.replace(/"/g, '"')
|
|
112
|
+
.replace(/'/g, ''');
|
|
113
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <foreach> tag - iterate over XML elements
|
|
3
|
+
* Usage: <foreach from="<available-subroutines />" as="sub">...</foreach>
|
|
4
|
+
* or: <foreach from="$varname" as="item">...</foreach>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DiracSession, DiracElement } from '../types/index.js';
|
|
8
|
+
import { setVariable, getVariable, substituteVariables } from '../runtime/session.js';
|
|
9
|
+
import { integrateChildren } from '../runtime/interpreter.js';
|
|
10
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
11
|
+
|
|
12
|
+
export async function executeForeach(session: DiracSession, element: DiracElement): Promise<void> {
|
|
13
|
+
const fromAttr = element.attributes.from;
|
|
14
|
+
const asVar = element.attributes.as || 'item';
|
|
15
|
+
const xpath = element.attributes.xpath; // Optional: filter like "//subroutine"
|
|
16
|
+
|
|
17
|
+
if (!fromAttr) {
|
|
18
|
+
throw new Error('<foreach> requires "from" attribute');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get the XML content (either from variable or inline content)
|
|
22
|
+
let xmlContent: string;
|
|
23
|
+
|
|
24
|
+
if (fromAttr.startsWith('$')) {
|
|
25
|
+
// Get from variable
|
|
26
|
+
const varName = fromAttr.substring(1);
|
|
27
|
+
xmlContent = getVariable(session, varName) || '';
|
|
28
|
+
} else if (fromAttr.startsWith('<')) {
|
|
29
|
+
// Evaluate the "from" attribute as inline XML (e.g., <available-subroutines />)
|
|
30
|
+
const savedOutput = session.output;
|
|
31
|
+
session.output = [];
|
|
32
|
+
|
|
33
|
+
// Parse and execute the from content
|
|
34
|
+
const { DiracParser } = await import('../runtime/parser.js');
|
|
35
|
+
const parser = new DiracParser();
|
|
36
|
+
try {
|
|
37
|
+
const fromElement = parser.parse(fromAttr);
|
|
38
|
+
const { integrate } = await import('../runtime/interpreter.js');
|
|
39
|
+
await integrate(session, fromElement);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
session.output = savedOutput;
|
|
42
|
+
throw new Error(`Failed to evaluate from="${fromAttr}": ${e}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
xmlContent = session.output.join('');
|
|
46
|
+
session.output = savedOutput;
|
|
47
|
+
} else {
|
|
48
|
+
// Treat as literal XML string
|
|
49
|
+
xmlContent = substituteVariables(session, fromAttr);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!xmlContent || xmlContent.trim() === '') {
|
|
53
|
+
return; // Nothing to iterate over
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse the XML content
|
|
57
|
+
const parser = new XMLParser({
|
|
58
|
+
ignoreAttributes: false,
|
|
59
|
+
attributeNamePrefix: '@_',
|
|
60
|
+
parseAttributeValue: false,
|
|
61
|
+
preserveOrder: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = parser.parse(`<root>${xmlContent}</root>`);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
throw new Error(`Failed to parse XML in <foreach>: ${e}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract elements to iterate over
|
|
72
|
+
const items = extractElements(parsed, xpath);
|
|
73
|
+
|
|
74
|
+
// Iterate over each item
|
|
75
|
+
for (const item of items) {
|
|
76
|
+
// Store the item as a variable (as an object with tag, attributes, text)
|
|
77
|
+
setVariable(session, asVar, item, false);
|
|
78
|
+
|
|
79
|
+
// Execute the loop body
|
|
80
|
+
await integrateChildren(session, element);
|
|
81
|
+
|
|
82
|
+
if (session.isBreak) {
|
|
83
|
+
session.isBreak = false;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (session.isReturn) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface XmlItem {
|
|
94
|
+
tag: string;
|
|
95
|
+
attributes: Record<string, string>;
|
|
96
|
+
text?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractElements(parsed: any, xpath?: string): XmlItem[] {
|
|
100
|
+
const items: XmlItem[] = [];
|
|
101
|
+
|
|
102
|
+
// parsed is array with preserveOrder
|
|
103
|
+
if (!Array.isArray(parsed)) {
|
|
104
|
+
return items;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Recursively extract all elements matching xpath
|
|
108
|
+
const extractFromNode = (node: any) => {
|
|
109
|
+
if (!node) return;
|
|
110
|
+
|
|
111
|
+
const tagName = Object.keys(node).find(k => k !== ':@' && k !== '#text' && k !== '#comment');
|
|
112
|
+
if (!tagName) return;
|
|
113
|
+
|
|
114
|
+
const item: XmlItem = {
|
|
115
|
+
tag: tagName,
|
|
116
|
+
attributes: {},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Extract attributes
|
|
120
|
+
if (node[':@']) {
|
|
121
|
+
for (const [key, value] of Object.entries(node[':@'])) {
|
|
122
|
+
if (key.startsWith('@_')) {
|
|
123
|
+
item.attributes[key.slice(2)] = value as string;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Extract text content and recurse into children
|
|
129
|
+
const children = node[tagName];
|
|
130
|
+
if (Array.isArray(children)) {
|
|
131
|
+
for (const child of children) {
|
|
132
|
+
if (child['#text']) {
|
|
133
|
+
item.text = (item.text || '') + child['#text'];
|
|
134
|
+
} else {
|
|
135
|
+
// Recurse into child elements
|
|
136
|
+
extractFromNode(child);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Add this item if it matches xpath
|
|
142
|
+
if (!xpath || matchesXPath(item, xpath)) {
|
|
143
|
+
items.push(item);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Start extraction from root
|
|
148
|
+
for (const rootItem of parsed) {
|
|
149
|
+
if (rootItem['root']) {
|
|
150
|
+
const rootChildren = rootItem['root'];
|
|
151
|
+
if (Array.isArray(rootChildren)) {
|
|
152
|
+
for (const child of rootChildren) {
|
|
153
|
+
extractFromNode(child);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return items;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function matchesXPath(item: XmlItem, xpath: string): boolean {
|
|
163
|
+
// Simple xpath matching - just tag name for now
|
|
164
|
+
// Supports: "//tagname" or "tagname"
|
|
165
|
+
const tagMatch = xpath.match(/\/{0,2}(\w+)/);
|
|
166
|
+
if (tagMatch) {
|
|
167
|
+
return item.tag === tagMatch[1];
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
package/src/tags/tag-check.ts
CHANGED
|
@@ -15,20 +15,27 @@ const SIMILARITY_CUTOFF = 0.75;
|
|
|
15
15
|
|
|
16
16
|
// Helper: get embedding server config from config.yml
|
|
17
17
|
function getEmbeddingServerConfig() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
try {
|
|
19
|
+
const configPath = process.env.DIRAC_CONFIG || 'config.yml';
|
|
20
|
+
const config = yaml.load(fs.readFileSync(configPath, 'utf8')) as any;
|
|
21
|
+
const host = config.embeddingServer?.host || 'localhost';
|
|
22
|
+
const port = config.embeddingServer?.port || 11434;
|
|
23
|
+
const model = config.embeddingServer?.model || 'embeddinggemma';
|
|
24
|
+
return { host, port, model };
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// Fallback to defaults if config file not found
|
|
27
|
+
return { host: 'localhost', port: 11434, model: 'embeddinggemma' };
|
|
28
|
+
}
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
// Helper: call Ollama embedding API directly
|
|
25
32
|
async function getEmbeddings(tags: string[]): Promise<number[][]> {
|
|
26
|
-
const { host, port } = getEmbeddingServerConfig();
|
|
33
|
+
const { host, port, model } = getEmbeddingServerConfig();
|
|
27
34
|
return await Promise.all(tags.map(async tag => {
|
|
28
35
|
const response = await fetch(`http://${host}:${port}/api/embeddings`, {
|
|
29
36
|
method: 'POST',
|
|
30
37
|
headers: { 'Content-Type': 'application/json' },
|
|
31
|
-
body: JSON.stringify({ model
|
|
38
|
+
body: JSON.stringify({ model, prompt: tag })
|
|
32
39
|
});
|
|
33
40
|
const data = await response.json();
|
|
34
41
|
return data.embedding;
|
|
@@ -67,6 +74,12 @@ export async function executeTagCheck(session: DiracSession, element: DiracEleme
|
|
|
67
74
|
|
|
68
75
|
for (const child of element.children) {
|
|
69
76
|
const tagName = child.tag;
|
|
77
|
+
|
|
78
|
+
// Skip text nodes (empty tag names) and whitespace
|
|
79
|
+
if (!tagName || tagName.trim() === '') {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
console.error(`[tag-check] Checking tag: <${tagName}>`);
|
|
71
84
|
let allValid = false;
|
|
72
85
|
let correctedTag: string | null = null;
|
package/src/test-runner.ts
CHANGED
|
@@ -33,10 +33,10 @@ export class TestRunner {
|
|
|
33
33
|
constructor(testDir: string = 'tests', configPath?: string) {
|
|
34
34
|
this.testDir = testDir;
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
if (fs.existsSync(
|
|
39
|
-
this.config = yaml.load(fs.readFileSync(
|
|
36
|
+
// For tests, always use ollama as the default provider (no API keys needed)
|
|
37
|
+
// Unless a specific test config is provided
|
|
38
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
39
|
+
this.config = yaml.load(fs.readFileSync(configPath, 'utf8')) || {};
|
|
40
40
|
} else {
|
|
41
41
|
this.config = { llmProvider: 'ollama', llmModel: 'llama2' };
|
|
42
42
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -23,23 +23,26 @@ export interface ValidationResult {
|
|
|
23
23
|
// Helper: get embedding server config from config.yml
|
|
24
24
|
function getEmbeddingServerConfig() {
|
|
25
25
|
try {
|
|
26
|
-
const
|
|
26
|
+
const configPath = process.env.DIRAC_CONFIG || 'config.yml';
|
|
27
|
+
const config = yaml.load(fs.readFileSync(configPath, 'utf8')) as any;
|
|
27
28
|
const host = config.embeddingServer?.host || 'localhost';
|
|
28
|
-
const port = config.embeddingServer?.port ||
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
const port = config.embeddingServer?.port || 11434;
|
|
30
|
+
const model = config.embeddingServer?.model || 'embeddinggemma';
|
|
31
|
+
return { host, port, model };
|
|
32
|
+
} catch (e) {
|
|
33
|
+
// Fallback to defaults if config file not found
|
|
34
|
+
return { host: 'localhost', port: 11434, model: 'embeddinggemma' };
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
// Helper: call Ollama embedding API directly
|
|
36
39
|
async function getEmbeddings(tags: string[]): Promise<number[][]> {
|
|
37
|
-
const { host, port } = getEmbeddingServerConfig();
|
|
40
|
+
const { host, port, model } = getEmbeddingServerConfig();
|
|
38
41
|
return await Promise.all(tags.map(async tag => {
|
|
39
42
|
const response = await fetch(`http://${host}:${port}/api/embeddings`, {
|
|
40
43
|
method: 'POST',
|
|
41
44
|
headers: { 'Content-Type': 'application/json' },
|
|
42
|
-
body: JSON.stringify({ model
|
|
45
|
+
body: JSON.stringify({ model, prompt: tag })
|
|
43
46
|
});
|
|
44
47
|
const data = await response.json();
|
|
45
48
|
return data.embedding;
|
|
@@ -179,7 +182,8 @@ export async function validateDiracCode(
|
|
|
179
182
|
|
|
180
183
|
// Recursively validate all elements
|
|
181
184
|
async function validateElement(element: DiracElement) {
|
|
182
|
-
|
|
185
|
+
// Skip text nodes and whitespace-only tags
|
|
186
|
+
if (element.tag && element.tag !== 'dirac' && element.tag.trim() !== '') {
|
|
183
187
|
const result = await validateTag(session, element, options);
|
|
184
188
|
results.push(result);
|
|
185
189
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!-- TEST: available-subroutines with foreach and attr -->
|
|
2
|
+
<!-- EXPECT: - show-subroutines:
|
|
3
|
+
- greet: Greets someone
|
|
4
|
+
- gender: Returns gender identity
|
|
5
|
+
- age: Returns the person age -->
|
|
6
|
+
<dirac>
|
|
7
|
+
<subroutine name="TestSuite">
|
|
8
|
+
<subroutine name="age" description="Returns the person age">
|
|
9
|
+
<output>42</output>
|
|
10
|
+
</subroutine>
|
|
11
|
+
|
|
12
|
+
<subroutine name="gender" description="Returns gender identity">
|
|
13
|
+
<output>Male</output>
|
|
14
|
+
</subroutine>
|
|
15
|
+
|
|
16
|
+
<subroutine name="greet" description="Greets someone">
|
|
17
|
+
<output>Hello!</output>
|
|
18
|
+
</subroutine>
|
|
19
|
+
|
|
20
|
+
<subroutine name="show-subroutines">
|
|
21
|
+
<defvar name="subs"><available-subroutines /></defvar>
|
|
22
|
+
<foreach from="$subs" as="sub" xpath="//subroutine">
|
|
23
|
+
<output>- <attr name="name" from="$sub" />: <attr name="description" from="$sub" />
|
|
24
|
+
</output>
|
|
25
|
+
</foreach>
|
|
26
|
+
</subroutine>
|
|
27
|
+
|
|
28
|
+
<show-subroutines />
|
|
29
|
+
</subroutine>
|
|
30
|
+
|
|
31
|
+
<TestSuite />
|
|
32
|
+
</dirac>
|