@pedrofariasx/qwenproxy 1.1.0
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/LICENSE +13 -0
- package/README.md +292 -0
- package/bin/qwenproxy.mjs +11 -0
- package/package.json +56 -0
- package/src/api/models.ts +183 -0
- package/src/api/server.ts +126 -0
- package/src/cache/memory-cache.ts +186 -0
- package/src/core/account-manager.ts +132 -0
- package/src/core/accounts.ts +78 -0
- package/src/core/config.ts +91 -0
- package/src/core/database.ts +92 -0
- package/src/core/logger.ts +96 -0
- package/src/core/metrics.ts +169 -0
- package/src/core/model-registry.ts +30 -0
- package/src/core/stream-registry.ts +40 -0
- package/src/core/watchdog.ts +130 -0
- package/src/index.ts +7 -0
- package/src/linter/extraction-engine.ts +165 -0
- package/src/linter/index.ts +258 -0
- package/src/linter/repair-normalize.ts +245 -0
- package/src/linter/safety-gate.ts +219 -0
- package/src/linter/streaming-state-machine.ts +252 -0
- package/src/linter/structural-parser.ts +352 -0
- package/src/linter/types.ts +74 -0
- package/src/login.ts +228 -0
- package/src/routes/chat.ts +801 -0
- package/src/routes/upload.ts +700 -0
- package/src/services/playwright.ts +778 -0
- package/src/services/qwen.ts +500 -0
- package/src/tests/advanced.test.ts +227 -0
- package/src/tests/agenticStress.test.ts +360 -0
- package/src/tests/concurrency.test.ts +103 -0
- package/src/tests/concurrentChat.test.ts +71 -0
- package/src/tests/delta.test.ts +63 -0
- package/src/tests/index.test.ts +356 -0
- package/src/tests/jsonFix.test.ts +98 -0
- package/src/tests/linter.test.ts +151 -0
- package/src/tests/parallel.test.ts +42 -0
- package/src/tests/parser.test.ts +89 -0
- package/src/tests/rotation.test.ts +45 -0
- package/src/tests/streamingOptimizations.test.ts +328 -0
- package/src/tests/structureVerification.test.ts +176 -0
- package/src/tools/ast.ts +15 -0
- package/src/tools/coercion.ts +67 -0
- package/src/tools/confidence.ts +48 -0
- package/src/tools/detector.ts +40 -0
- package/src/tools/executor.ts +236 -0
- package/src/tools/parser.ts +446 -0
- package/src/tools/pipeline.ts +122 -0
- package/src/tools/registry-runtime.ts +34 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/repair.ts +42 -0
- package/src/tools/schema.ts +285 -0
- package/src/tools/types.ts +104 -0
- package/src/tools/validator.ts +33 -0
- package/src/utils/context-truncation.ts +61 -0
- package/src/utils/json.ts +114 -0
- package/src/utils/qwen-stream-parser.ts +286 -0
- package/src/utils/types.ts +101 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Layer 5: Validation + Safety Gate
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { CanonicalToolCall, ToolCallSource, SecurityViolation, ToolDefinition } from './types'
|
|
6
|
+
|
|
7
|
+
export interface ValidationReport {
|
|
8
|
+
isValid: boolean
|
|
9
|
+
violations: SecurityViolation[]
|
|
10
|
+
confidence: number
|
|
11
|
+
warnings: string[]
|
|
12
|
+
canExecute: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SafetyGate {
|
|
16
|
+
private registry: Record<string, ToolDefinition> = {}
|
|
17
|
+
|
|
18
|
+
private readonly dangerousPatterns = [
|
|
19
|
+
/\$\(/g, /\$\{/g, /`/g,
|
|
20
|
+
/;\s*(rm|cat|ls|whoami|id|curl|wget|nc|ncat)\b/gi,
|
|
21
|
+
/\|\s*(sh|bash|zsh|csh|python|node|perl)\b/gi,
|
|
22
|
+
/&&\s*(rm|cat|whoami)/gi,
|
|
23
|
+
/exec\s*\(/gi, /eval\s*\(/gi, /\beval\s*\(/gi,
|
|
24
|
+
/system\s*\(/gi, /passthru\s*\(/gi, /shell_exec\s*\(/gi,
|
|
25
|
+
/proc\s*\(/gi, /subprocess\s*\./gi, /spawn\s*\(/gi,
|
|
26
|
+
/execSync\s*\(/gi, /child_process/gi,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
private readonly ssrfPatterns = [
|
|
30
|
+
/http[s]?:\/\/(?:127\.0\.0\.1|0\.0\.0\.0|\[::1\]|localhost|169\.254\.\d+\.\d+|metadata\.google)/i,
|
|
31
|
+
/gopher:\/\//i, /file:\/\//i, /ftp:\/\//i,
|
|
32
|
+
/\b7777\b|\b9090\b|\b3389\b|\b22\b/,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
private readonly promptInjectionPatterns = [
|
|
36
|
+
/ignore\s+(all\s+)?previous\s+instructions/i, /disregard\s+(all\s+)?previous/i,
|
|
37
|
+
/jailbreak/i, /you\s+are\s+now\s+DAN/i, /pretend\s+you\s+are/i,
|
|
38
|
+
/act\s+as\s+if/i, /your\s+new\s+instructions/i, /developer\s+mode/i,
|
|
39
|
+
/list\s+all\s+files/i, /cat\s+\/etc/i,
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
private readonly encodedPatterns = [
|
|
43
|
+
/%3Cscript/i, /javascript:/i, /\\x[0-9a-fA-F]{2}/, /\\u[0-9a-fA-F]{4}/,
|
|
44
|
+
/&#[0-9]+;/i, /data:text\/html/i,
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
registerTool(name: string, definition: Partial<ToolDefinition>): void {
|
|
48
|
+
this.registry[name] = {
|
|
49
|
+
name,
|
|
50
|
+
description: definition.description ?? '',
|
|
51
|
+
parameters: definition.parameters ?? {},
|
|
52
|
+
required: definition.required ?? [],
|
|
53
|
+
strict: definition.strict ?? false,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
registerRegistry(registry: Record<string, ToolDefinition>): void {
|
|
58
|
+
this.registry = registry
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
validate(call: CanonicalToolCall, sourceHint: ToolCallSource): ValidationReport {
|
|
62
|
+
const violations: SecurityViolation[] = []
|
|
63
|
+
const warnings: string[] = []
|
|
64
|
+
let confidence = call.meta?.confidence ?? 0.5
|
|
65
|
+
|
|
66
|
+
const semanticResult = this.validateSemantics(call)
|
|
67
|
+
violations.push(...semanticResult.violations)
|
|
68
|
+
warnings.push(...semanticResult.warnings)
|
|
69
|
+
confidence = Math.min(confidence, semanticResult.confidence)
|
|
70
|
+
|
|
71
|
+
const securityResult = this.validateSecurity(call)
|
|
72
|
+
violations.push(...securityResult.violations)
|
|
73
|
+
warnings.push(...securityResult.warnings)
|
|
74
|
+
|
|
75
|
+
warnings.push(...this.validateTypes(call).warnings)
|
|
76
|
+
|
|
77
|
+
const registryResult = this.validateRegistry(call)
|
|
78
|
+
warnings.push(...registryResult.warnings)
|
|
79
|
+
confidence = Math.min(confidence, registryResult.confidence)
|
|
80
|
+
|
|
81
|
+
const hasBlockingViolations = violations.some(v =>
|
|
82
|
+
v.type === 'shell_injection' || v.type === 'filesystem_access' || v.type === 'prompt_injection'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
confidence = Math.max(0.1, Math.min(1.0, confidence))
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
isValid: !hasBlockingViolations && violations.length === 0,
|
|
89
|
+
violations,
|
|
90
|
+
confidence,
|
|
91
|
+
warnings,
|
|
92
|
+
canExecute: !hasBlockingViolations,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private validateSemantics(call: CanonicalToolCall): { violations: SecurityViolation[]; warnings: string[]; confidence: number } {
|
|
97
|
+
const violations: SecurityViolation[] = []
|
|
98
|
+
const warnings: string[] = []
|
|
99
|
+
let confidence = 1.0
|
|
100
|
+
|
|
101
|
+
if (!call.tool || call.tool.trim().length === 0) {
|
|
102
|
+
violations.push({ type: 'shell_injection', field: 'tool', value: String(call.tool), detail: 'Tool name is empty' })
|
|
103
|
+
confidence -= 0.5
|
|
104
|
+
}
|
|
105
|
+
if (typeof call.tool !== 'string') {
|
|
106
|
+
violations.push({ type: 'shell_injection', field: 'tool', value: String(call.tool), detail: 'Tool name is not a string' })
|
|
107
|
+
confidence -= 0.4
|
|
108
|
+
}
|
|
109
|
+
if (call.tool && /[ /\\]/.test(call.tool)) {
|
|
110
|
+
warnings.push(`Suspicious tool name: "${call.tool}"`)
|
|
111
|
+
confidence -= 0.1
|
|
112
|
+
}
|
|
113
|
+
if (typeof call.input !== 'object' || call.input === null || Array.isArray(call.input)) {
|
|
114
|
+
violations.push({ type: 'shell_injection', field: 'input', value: String(call.input), detail: 'Input is not a valid object' })
|
|
115
|
+
confidence -= 0.4
|
|
116
|
+
}
|
|
117
|
+
for (const [key, value] of Object.entries(call.input)) {
|
|
118
|
+
if (typeof value === 'string' && value.trim().length === 0) {
|
|
119
|
+
warnings.push(`Empty string parameter: "${key}"`)
|
|
120
|
+
confidence -= 0.05
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { violations, warnings, confidence: Math.max(0, confidence) }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private validateSecurity(call: CanonicalToolCall): { violations: SecurityViolation[]; warnings: string[] } {
|
|
128
|
+
const violations: SecurityViolation[] = []
|
|
129
|
+
const warnings: string[] = []
|
|
130
|
+
const inputString = JSON.stringify(call.input)
|
|
131
|
+
|
|
132
|
+
for (const pattern of this.dangerousPatterns) {
|
|
133
|
+
pattern.lastIndex = 0
|
|
134
|
+
if (pattern.test(inputString)) {
|
|
135
|
+
violations.push({ type: 'shell_injection', field: 'input', value: inputString.slice(0, 100), detail: `Potential shell injection pattern detected` })
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const field of Object.values(call.input)) {
|
|
141
|
+
const str = String(field ?? '')
|
|
142
|
+
for (const pattern of this.ssrfPatterns) {
|
|
143
|
+
pattern.lastIndex = 0
|
|
144
|
+
if (pattern.test(str)) {
|
|
145
|
+
violations.push({ type: 'ssrf', field: 'input', value: str.slice(0, 200), detail: `SSRF pattern detected` })
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const combinedInput = Object.values(call.input)
|
|
152
|
+
.map(v => typeof v === 'string' ? v : JSON.stringify(v))
|
|
153
|
+
.join(' ')
|
|
154
|
+
|
|
155
|
+
for (const pattern of this.promptInjectionPatterns) {
|
|
156
|
+
pattern.lastIndex = 0
|
|
157
|
+
if (pattern.test(combinedInput)) {
|
|
158
|
+
violations.push({ type: 'prompt_injection', field: 'input', value: combinedInput.slice(0, 200), detail: `Prompt injection pattern detected` })
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const pattern of this.encodedPatterns) {
|
|
164
|
+
pattern.lastIndex = 0
|
|
165
|
+
if (pattern.test(combinedInput)) {
|
|
166
|
+
violations.push({ type: 'encoded_payload', field: 'input', value: combinedInput.slice(0, 200), detail: `Encoded payload detected` })
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { violations, warnings }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private validateTypes(call: CanonicalToolCall): { warnings: string[] } {
|
|
175
|
+
const warnings: string[] = []
|
|
176
|
+
for (const [key, value] of Object.entries(call.input)) {
|
|
177
|
+
const type = typeof value
|
|
178
|
+
if (value !== null && typeof value !== 'undefined' && typeof value !== 'function' && typeof value !== 'symbol' && typeof value !== 'bigint' && typeof value !== 'object') {
|
|
179
|
+
warnings.push(`Unexpected type for parameter "${key}": ${type}`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { warnings }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private validateRegistry(call: CanonicalToolCall): { warnings: string[]; confidence: number } {
|
|
186
|
+
const warnings: string[] = []
|
|
187
|
+
let confidence = 1.0
|
|
188
|
+
|
|
189
|
+
const toolDef = this.registry[call.tool]
|
|
190
|
+
if (!toolDef) {
|
|
191
|
+
warnings.push(`Tool "${call.tool}" not found in registry`)
|
|
192
|
+
confidence -= 0.3
|
|
193
|
+
return { warnings, confidence: Math.max(0.1, confidence) }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (toolDef.required && Array.isArray(toolDef.required)) {
|
|
197
|
+
for (const requiredField of toolDef.required) {
|
|
198
|
+
if (!(requiredField in call.input) || call.input[requiredField] === undefined) {
|
|
199
|
+
warnings.push(`Missing required field: "${requiredField}" for tool "${call.tool}"`)
|
|
200
|
+
confidence -= 0.2
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (toolDef.strict && toolDef.parameters) {
|
|
206
|
+
for (const [param, expectedType] of Object.entries(toolDef.parameters)) {
|
|
207
|
+
if (param in call.input && call.input[param] !== undefined) {
|
|
208
|
+
const actualType = typeof call.input[param]
|
|
209
|
+
if (actualType !== String(expectedType).toLowerCase()) {
|
|
210
|
+
warnings.push(`Type mismatch for "${param}": expected ${String(expectedType)}, got ${actualType}`)
|
|
211
|
+
confidence -= 0.1
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { warnings, confidence: Math.max(0.1, confidence) }
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Layer 1: Streaming State Machine
|
|
3
|
+
* Incremental parser that processes token-by-token chunks
|
|
4
|
+
* Maintains structural state without requiring complete input
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ParserState } from './types'
|
|
8
|
+
|
|
9
|
+
export interface StreamChunkResult {
|
|
10
|
+
buffer: string
|
|
11
|
+
triggersExtraction: boolean
|
|
12
|
+
inProgress: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class StreamingStateMachine {
|
|
16
|
+
private state: ParserState = {
|
|
17
|
+
buffer: '',
|
|
18
|
+
insideCodeBlock: false,
|
|
19
|
+
braceDepth: 0,
|
|
20
|
+
bracketDepth: 0,
|
|
21
|
+
inString: false,
|
|
22
|
+
escapeNext: false,
|
|
23
|
+
potentialToolStart: false,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private lastChunkEndsWithJson: boolean = false
|
|
27
|
+
|
|
28
|
+
reset(): void {
|
|
29
|
+
this.state = {
|
|
30
|
+
buffer: '',
|
|
31
|
+
insideCodeBlock: false,
|
|
32
|
+
braceDepth: 0,
|
|
33
|
+
bracketDepth: 0,
|
|
34
|
+
inString: false,
|
|
35
|
+
escapeNext: false,
|
|
36
|
+
potentialToolStart: false,
|
|
37
|
+
}
|
|
38
|
+
this.lastChunkEndsWithJson = false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getState(): ParserState {
|
|
42
|
+
return { ...this.state }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isInsideStructuredValue(): boolean {
|
|
46
|
+
return this.state.braceDepth > 0 || this.state.bracketDepth > 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
push(chunk: string): StreamChunkResult {
|
|
50
|
+
this.state.buffer += chunk
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
53
|
+
const char = chunk[i]
|
|
54
|
+
this.processChar(char)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const triggersExtraction = this.shouldAttemptExtraction()
|
|
58
|
+
const inProgress = this.isInsideStructuredValue() || this.looksLikeReAct(chunk)
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
buffer: this.state.buffer,
|
|
62
|
+
triggersExtraction,
|
|
63
|
+
inProgress,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private processChar(char: string): void {
|
|
68
|
+
const { inString, escapeNext } = this.state
|
|
69
|
+
|
|
70
|
+
if (escapeNext) {
|
|
71
|
+
this.state.escapeNext = false
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (char === '\\' && inString) {
|
|
76
|
+
this.state.escapeNext = true
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!inString) {
|
|
81
|
+
if (char === '"') {
|
|
82
|
+
this.state.inString = true
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (char === '{') {
|
|
87
|
+
this.state.braceDepth++
|
|
88
|
+
this.state.potentialToolStart = true
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (char === '}') {
|
|
93
|
+
if (this.state.braceDepth > 0) this.state.braceDepth--
|
|
94
|
+
this.updatePotentialToolStart()
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (char === '[') {
|
|
99
|
+
this.state.bracketDepth++
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (char === ']') {
|
|
104
|
+
if (this.state.bracketDepth > 0) this.state.bracketDepth--
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (char === '`') {
|
|
109
|
+
this.state.insideCodeBlock = !this.state.insideCodeBlock
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
if (char === '"') {
|
|
114
|
+
this.state.inString = false
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private updatePotentialToolStart(): void {
|
|
121
|
+
const { buffer, braceDepth } = this.state
|
|
122
|
+
if (braceDepth > 0) return
|
|
123
|
+
|
|
124
|
+
const trimmed = buffer.trimEnd()
|
|
125
|
+
if (trimmed.length === 0) {
|
|
126
|
+
this.state.potentialToolStart = false
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const lastBlock = this.extractLastBlock(trimmed)
|
|
131
|
+
if (lastBlock) {
|
|
132
|
+
this.state.potentialToolStart = true
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private extractLastBlock(text: string): string | null {
|
|
137
|
+
let depth = 0
|
|
138
|
+
let start = -1
|
|
139
|
+
|
|
140
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
141
|
+
const char = text[i]
|
|
142
|
+
if (char === '}') {
|
|
143
|
+
if (depth === 0) start = i
|
|
144
|
+
depth++
|
|
145
|
+
} else if (char === '{') {
|
|
146
|
+
depth--
|
|
147
|
+
if (depth === 0 && start !== -1) {
|
|
148
|
+
return text.slice(i, start + 1)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private looksLikeReAct(chunk: string): boolean {
|
|
157
|
+
const markers = ['Action:', 'Action Input:', 'Thought:']
|
|
158
|
+
|
|
159
|
+
for (const marker of markers) {
|
|
160
|
+
if (chunk.includes(marker)) return true
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private shouldAttemptExtraction(): boolean {
|
|
167
|
+
const { braceDepth, buffer } = this.state
|
|
168
|
+
|
|
169
|
+
if (braceDepth === 0 && buffer.trim().length > 0) {
|
|
170
|
+
const trimmed = buffer.trimEnd()
|
|
171
|
+
return trimmed.endsWith('}') || trimmed.endsWith(']')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
extractCompleteSpans(): string[] {
|
|
178
|
+
const spans: string[] = []
|
|
179
|
+
const { buffer, braceDepth } = this.state
|
|
180
|
+
|
|
181
|
+
if (braceDepth !== 0) return spans
|
|
182
|
+
|
|
183
|
+
const trimmed = buffer.trim()
|
|
184
|
+
if (trimmed.length === 0) return spans
|
|
185
|
+
|
|
186
|
+
// Extract JSON objects/arrays
|
|
187
|
+
let depth = 0
|
|
188
|
+
let start = -1
|
|
189
|
+
let inString = false
|
|
190
|
+
let escape = false
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
193
|
+
const char = trimmed[i]
|
|
194
|
+
|
|
195
|
+
if (escape) {
|
|
196
|
+
escape = false
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (char === '\\' && inString) {
|
|
201
|
+
escape = true
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (char === '"' && !escape) {
|
|
206
|
+
inString = !inString
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (inString) continue
|
|
211
|
+
|
|
212
|
+
if (char === '{' || char === '[') {
|
|
213
|
+
if (depth === 0) start = i
|
|
214
|
+
depth++
|
|
215
|
+
} else if (char === '}' || char === ']') {
|
|
216
|
+
depth--
|
|
217
|
+
if (depth === 0 && start !== -1) {
|
|
218
|
+
spans.push(trimmed.slice(start, i + 1))
|
|
219
|
+
start = -1
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check for ReAct patterns
|
|
225
|
+
const reactMatch = trimmed.match(/Action:\s*(\w+)\s*\n?Action Input:\s*(\{[\s\S]*\})/) ||
|
|
226
|
+
trimmed.match(/Action:\s*(\w+)\s*Action Input:\s*(\{[\s\S]*\})/)
|
|
227
|
+
|
|
228
|
+
if (reactMatch && !spans.includes(reactMatch[2])) {
|
|
229
|
+
spans.push(reactMatch[2])
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return spans
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
hasCompleteContent(): boolean {
|
|
236
|
+
const { braceDepth, buffer } = this.state
|
|
237
|
+
|
|
238
|
+
if (braceDepth !== 0) return false
|
|
239
|
+
|
|
240
|
+
const trimmed = buffer.trim()
|
|
241
|
+
return trimmed.includes('{') || trimmed.includes('[') ||
|
|
242
|
+
/Action:\s*\w+\s*\n?Action Input:/.test(trimmed)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getBuffer(): string {
|
|
246
|
+
return this.state.buffer
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
setBuffer(buffer: string): void {
|
|
250
|
+
this.state.buffer = buffer
|
|
251
|
+
}
|
|
252
|
+
}
|