cognitive-modules-cli 1.2.0 → 1.4.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/README.md +21 -13
- package/dist/cli.js +35 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +7 -1
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.js +344 -0
- package/dist/modules/index.d.ts +1 -0
- package/dist/modules/index.js +1 -0
- package/dist/modules/loader.js +7 -2
- package/dist/modules/runner.d.ts +44 -2
- package/dist/modules/runner.js +611 -4
- package/dist/modules/subagent.d.ts +65 -0
- package/dist/modules/subagent.js +185 -0
- package/dist/providers/base.d.ts +45 -1
- package/dist/providers/base.js +67 -0
- package/dist/providers/openai.d.ts +27 -3
- package/dist/providers/openai.js +175 -3
- package/dist/server/http.d.ts +20 -0
- package/dist/server/http.js +243 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +4 -0
- package/dist/types.d.ts +208 -1
- package/dist/types.js +82 -1
- package/package.json +4 -1
- package/src/cli.ts +36 -1
- package/src/index.ts +11 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/server.ts +403 -0
- package/src/modules/index.ts +1 -0
- package/src/modules/loader.ts +8 -2
- package/src/modules/runner.ts +803 -5
- package/src/modules/subagent.ts +275 -0
- package/src/providers/base.ts +86 -1
- package/src/providers/openai.ts +226 -4
- package/src/server/http.ts +316 -0
- package/src/server/index.ts +6 -0
- package/src/types.ts +319 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent - Orchestrate module calls with isolated execution contexts.
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - @call:module-name - Call another module
|
|
6
|
+
* - @call:module-name(args) - Call with arguments
|
|
7
|
+
* - context: fork - Isolated execution (no shared state)
|
|
8
|
+
* - context: main - Shared execution (default)
|
|
9
|
+
*/
|
|
10
|
+
import { findModule, getDefaultSearchPaths } from './loader.js';
|
|
11
|
+
import { runModule } from './runner.js';
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// Context Management
|
|
14
|
+
// =============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Create a new root context
|
|
17
|
+
*/
|
|
18
|
+
export function createContext(maxDepth = 5) {
|
|
19
|
+
return {
|
|
20
|
+
parentId: null,
|
|
21
|
+
depth: 0,
|
|
22
|
+
maxDepth,
|
|
23
|
+
results: {},
|
|
24
|
+
isolated: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Fork context (isolated - no inherited results)
|
|
29
|
+
*/
|
|
30
|
+
export function forkContext(ctx, moduleName) {
|
|
31
|
+
return {
|
|
32
|
+
parentId: moduleName,
|
|
33
|
+
depth: ctx.depth + 1,
|
|
34
|
+
maxDepth: ctx.maxDepth,
|
|
35
|
+
results: {},
|
|
36
|
+
isolated: true
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Extend context (shared - inherits results)
|
|
41
|
+
*/
|
|
42
|
+
export function extendContext(ctx, moduleName) {
|
|
43
|
+
return {
|
|
44
|
+
parentId: moduleName,
|
|
45
|
+
depth: ctx.depth + 1,
|
|
46
|
+
maxDepth: ctx.maxDepth,
|
|
47
|
+
results: { ...ctx.results },
|
|
48
|
+
isolated: false
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// =============================================================================
|
|
52
|
+
// Call Parsing
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Pattern to match @call:module-name or @call:module-name(args)
|
|
55
|
+
const CALL_PATTERN = /@call:([a-zA-Z0-9_-]+)(?:\(([^)]*)\))?/g;
|
|
56
|
+
/**
|
|
57
|
+
* Parse @call directives from text
|
|
58
|
+
*/
|
|
59
|
+
export function parseCalls(text) {
|
|
60
|
+
const calls = [];
|
|
61
|
+
let match;
|
|
62
|
+
// Reset regex state
|
|
63
|
+
CALL_PATTERN.lastIndex = 0;
|
|
64
|
+
while ((match = CALL_PATTERN.exec(text)) !== null) {
|
|
65
|
+
calls.push({
|
|
66
|
+
module: match[1],
|
|
67
|
+
args: match[2] || '',
|
|
68
|
+
match: match[0]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return calls;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Replace @call directives with their results
|
|
75
|
+
*/
|
|
76
|
+
export function substituteCallResults(text, callResults) {
|
|
77
|
+
let result = text;
|
|
78
|
+
for (const [callStr, callResult] of Object.entries(callResults)) {
|
|
79
|
+
const resultStr = typeof callResult === 'object'
|
|
80
|
+
? JSON.stringify(callResult, null, 2)
|
|
81
|
+
: String(callResult);
|
|
82
|
+
result = result.replace(callStr, `[Result from ${callStr}]:\n${resultStr}`);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// Orchestrator
|
|
88
|
+
// =============================================================================
|
|
89
|
+
export class SubagentOrchestrator {
|
|
90
|
+
provider;
|
|
91
|
+
running = new Set();
|
|
92
|
+
cwd;
|
|
93
|
+
constructor(provider, cwd = process.cwd()) {
|
|
94
|
+
this.provider = provider;
|
|
95
|
+
this.cwd = cwd;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Run a module with subagent support.
|
|
99
|
+
* Recursively resolves @call directives before final execution.
|
|
100
|
+
*/
|
|
101
|
+
async run(moduleName, options = {}, context) {
|
|
102
|
+
const { input = {}, validateInput = true, validateOutput = true, maxDepth = 5 } = options;
|
|
103
|
+
// Initialize context
|
|
104
|
+
const ctx = context ?? createContext(maxDepth);
|
|
105
|
+
// Check depth limit
|
|
106
|
+
if (ctx.depth > ctx.maxDepth) {
|
|
107
|
+
throw new Error(`Max subagent depth (${ctx.maxDepth}) exceeded. Check for circular calls.`);
|
|
108
|
+
}
|
|
109
|
+
// Prevent circular calls
|
|
110
|
+
if (this.running.has(moduleName)) {
|
|
111
|
+
throw new Error(`Circular call detected: ${moduleName}`);
|
|
112
|
+
}
|
|
113
|
+
this.running.add(moduleName);
|
|
114
|
+
try {
|
|
115
|
+
// Find and load module
|
|
116
|
+
const searchPaths = getDefaultSearchPaths(this.cwd);
|
|
117
|
+
const module = await findModule(moduleName, searchPaths);
|
|
118
|
+
if (!module) {
|
|
119
|
+
throw new Error(`Module not found: ${moduleName}`);
|
|
120
|
+
}
|
|
121
|
+
// Check if this module wants isolated execution
|
|
122
|
+
const moduleContextMode = module.context ?? 'main';
|
|
123
|
+
// Parse @call directives from prompt
|
|
124
|
+
const calls = parseCalls(module.prompt);
|
|
125
|
+
const callResults = {};
|
|
126
|
+
// Resolve each @call directive
|
|
127
|
+
for (const call of calls) {
|
|
128
|
+
const childModule = call.module;
|
|
129
|
+
const childArgs = call.args;
|
|
130
|
+
// Prepare child input
|
|
131
|
+
const childInput = childArgs
|
|
132
|
+
? { query: childArgs, code: childArgs }
|
|
133
|
+
: { ...input };
|
|
134
|
+
// Determine child context
|
|
135
|
+
const childContext = moduleContextMode === 'fork'
|
|
136
|
+
? forkContext(ctx, moduleName)
|
|
137
|
+
: extendContext(ctx, moduleName);
|
|
138
|
+
// Recursively run child module
|
|
139
|
+
const childResult = await this.run(childModule, {
|
|
140
|
+
input: childInput,
|
|
141
|
+
validateInput: false, // Skip validation for @call args
|
|
142
|
+
validateOutput
|
|
143
|
+
}, childContext);
|
|
144
|
+
// Store result
|
|
145
|
+
if (childResult.ok && 'data' in childResult) {
|
|
146
|
+
callResults[call.match] = childResult.data;
|
|
147
|
+
}
|
|
148
|
+
else if ('error' in childResult) {
|
|
149
|
+
callResults[call.match] = { error: childResult.error };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Substitute call results into prompt
|
|
153
|
+
let modifiedModule = module;
|
|
154
|
+
if (Object.keys(callResults).length > 0) {
|
|
155
|
+
const modifiedPrompt = substituteCallResults(module.prompt, callResults);
|
|
156
|
+
modifiedModule = {
|
|
157
|
+
...module,
|
|
158
|
+
prompt: modifiedPrompt + '\n\n## Subagent Results Available\nThe @call results have been injected above. Use them in your response.\n'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// Run the module
|
|
162
|
+
const result = await runModule(modifiedModule, this.provider, {
|
|
163
|
+
input,
|
|
164
|
+
verbose: false,
|
|
165
|
+
useV22: true
|
|
166
|
+
});
|
|
167
|
+
// Store result in context
|
|
168
|
+
if (result.ok && 'data' in result) {
|
|
169
|
+
ctx.results[moduleName] = result.data;
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
this.running.delete(moduleName);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Convenience function to run a module with subagent support
|
|
180
|
+
*/
|
|
181
|
+
export async function runWithSubagents(moduleName, provider, options = {}) {
|
|
182
|
+
const { cwd = process.cwd(), ...runOptions } = options;
|
|
183
|
+
const orchestrator = new SubagentOrchestrator(provider, cwd);
|
|
184
|
+
return orchestrator.run(moduleName, runOptions);
|
|
185
|
+
}
|
package/dist/providers/base.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Base Provider - Abstract class for all LLM providers
|
|
3
|
+
* v2.5: Added streaming and multimodal support
|
|
3
4
|
*/
|
|
4
|
-
import type { Provider, InvokeParams, InvokeResult } from '../types.js';
|
|
5
|
+
import type { Provider, InvokeParams, InvokeResult, ProviderV25, InvokeParamsV25, StreamingInvokeResult, ModalityType } from '../types.js';
|
|
5
6
|
export declare abstract class BaseProvider implements Provider {
|
|
6
7
|
abstract name: string;
|
|
7
8
|
abstract invoke(params: InvokeParams): Promise<InvokeResult>;
|
|
@@ -9,3 +10,46 @@ export declare abstract class BaseProvider implements Provider {
|
|
|
9
10
|
protected buildJsonPrompt(schema: object): string;
|
|
10
11
|
protected parseJsonResponse(content: string): unknown;
|
|
11
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Base Provider with v2.5 streaming and multimodal support
|
|
15
|
+
*/
|
|
16
|
+
export declare abstract class BaseProviderV25 extends BaseProvider implements ProviderV25 {
|
|
17
|
+
/**
|
|
18
|
+
* Check if this provider supports streaming
|
|
19
|
+
* Override in subclass to enable streaming
|
|
20
|
+
*/
|
|
21
|
+
supportsStreaming(): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Check if this provider supports multimodal input/output
|
|
24
|
+
* Override in subclass to enable multimodal
|
|
25
|
+
*/
|
|
26
|
+
supportsMultimodal(): {
|
|
27
|
+
input: ModalityType[];
|
|
28
|
+
output: ModalityType[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Invoke with streaming response
|
|
32
|
+
* Override in subclass to implement streaming
|
|
33
|
+
*/
|
|
34
|
+
invokeStream(params: InvokeParamsV25): Promise<StreamingInvokeResult>;
|
|
35
|
+
/**
|
|
36
|
+
* Format media inputs for the specific provider API
|
|
37
|
+
* Override in subclass for provider-specific formatting
|
|
38
|
+
*/
|
|
39
|
+
protected formatMediaForProvider(images?: Array<{
|
|
40
|
+
type: string;
|
|
41
|
+
url?: string;
|
|
42
|
+
data?: string;
|
|
43
|
+
media_type?: string;
|
|
44
|
+
}>, _audio?: Array<{
|
|
45
|
+
type: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
data?: string;
|
|
48
|
+
media_type?: string;
|
|
49
|
+
}>, _video?: Array<{
|
|
50
|
+
type: string;
|
|
51
|
+
url?: string;
|
|
52
|
+
data?: string;
|
|
53
|
+
media_type?: string;
|
|
54
|
+
}>): unknown[];
|
|
55
|
+
}
|
package/dist/providers/base.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Base Provider - Abstract class for all LLM providers
|
|
3
|
+
* v2.5: Added streaming and multimodal support
|
|
3
4
|
*/
|
|
4
5
|
export class BaseProvider {
|
|
5
6
|
buildJsonPrompt(schema) {
|
|
@@ -17,3 +18,69 @@ export class BaseProvider {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Base Provider with v2.5 streaming and multimodal support
|
|
23
|
+
*/
|
|
24
|
+
export class BaseProviderV25 extends BaseProvider {
|
|
25
|
+
/**
|
|
26
|
+
* Check if this provider supports streaming
|
|
27
|
+
* Override in subclass to enable streaming
|
|
28
|
+
*/
|
|
29
|
+
supportsStreaming() {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if this provider supports multimodal input/output
|
|
34
|
+
* Override in subclass to enable multimodal
|
|
35
|
+
*/
|
|
36
|
+
supportsMultimodal() {
|
|
37
|
+
return {
|
|
38
|
+
input: ['text'],
|
|
39
|
+
output: ['text']
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Invoke with streaming response
|
|
44
|
+
* Override in subclass to implement streaming
|
|
45
|
+
*/
|
|
46
|
+
async invokeStream(params) {
|
|
47
|
+
// Default: fallback to non-streaming with async generator wrapper
|
|
48
|
+
const result = await this.invoke(params);
|
|
49
|
+
async function* generateChunks() {
|
|
50
|
+
yield result.content;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
stream: generateChunks(),
|
|
54
|
+
usage: result.usage
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Format media inputs for the specific provider API
|
|
59
|
+
* Override in subclass for provider-specific formatting
|
|
60
|
+
*/
|
|
61
|
+
formatMediaForProvider(images, _audio, _video) {
|
|
62
|
+
// Default implementation for image-only providers (like OpenAI Vision)
|
|
63
|
+
if (!images || images.length === 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return images.map(img => {
|
|
67
|
+
if (img.type === 'url' && img.url) {
|
|
68
|
+
return {
|
|
69
|
+
type: 'image_url',
|
|
70
|
+
image_url: {
|
|
71
|
+
url: img.url
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
else if (img.type === 'base64' && img.data && img.media_type) {
|
|
76
|
+
return {
|
|
77
|
+
type: 'image_url',
|
|
78
|
+
image_url: {
|
|
79
|
+
url: `data:${img.media_type};base64,${img.data}`
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}).filter(Boolean);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI Provider - OpenAI API (and compatible APIs)
|
|
3
|
+
* v2.5: Added streaming and multimodal (vision) support
|
|
3
4
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import type { InvokeParams, InvokeResult } from '../types.js';
|
|
6
|
-
export declare class OpenAIProvider extends
|
|
5
|
+
import { BaseProviderV25 } from './base.js';
|
|
6
|
+
import type { InvokeParams, InvokeResult, InvokeParamsV25, StreamingInvokeResult, ModalityType } from '../types.js';
|
|
7
|
+
export declare class OpenAIProvider extends BaseProviderV25 {
|
|
7
8
|
name: string;
|
|
8
9
|
private apiKey;
|
|
9
10
|
private model;
|
|
10
11
|
private baseUrl;
|
|
11
12
|
constructor(apiKey?: string, model?: string, baseUrl?: string);
|
|
12
13
|
isConfigured(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Check if streaming is supported (always true for OpenAI)
|
|
16
|
+
*/
|
|
17
|
+
supportsStreaming(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Check multimodal support (vision models)
|
|
20
|
+
*/
|
|
21
|
+
supportsMultimodal(): {
|
|
22
|
+
input: ModalityType[];
|
|
23
|
+
output: ModalityType[];
|
|
24
|
+
};
|
|
13
25
|
invoke(params: InvokeParams): Promise<InvokeResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Invoke with streaming response
|
|
28
|
+
*/
|
|
29
|
+
invokeStream(params: InvokeParamsV25): Promise<StreamingInvokeResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Build messages with multimodal content (images)
|
|
32
|
+
*/
|
|
33
|
+
private buildMessagesWithMedia;
|
|
34
|
+
/**
|
|
35
|
+
* Convert MediaInput to URL for OpenAI API
|
|
36
|
+
*/
|
|
37
|
+
private mediaInputToUrl;
|
|
14
38
|
}
|
package/dist/providers/openai.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenAI Provider - OpenAI API (and compatible APIs)
|
|
3
|
+
* v2.5: Added streaming and multimodal (vision) support
|
|
3
4
|
*/
|
|
4
|
-
import {
|
|
5
|
-
export class OpenAIProvider extends
|
|
5
|
+
import { BaseProviderV25 } from './base.js';
|
|
6
|
+
export class OpenAIProvider extends BaseProviderV25 {
|
|
6
7
|
name = 'openai';
|
|
7
8
|
apiKey;
|
|
8
9
|
model;
|
|
9
10
|
baseUrl;
|
|
10
|
-
constructor(apiKey, model = 'gpt-
|
|
11
|
+
constructor(apiKey, model = 'gpt-4o', baseUrl = 'https://api.openai.com/v1') {
|
|
11
12
|
super();
|
|
12
13
|
this.apiKey = apiKey || process.env.OPENAI_API_KEY || '';
|
|
13
14
|
this.model = model;
|
|
@@ -16,6 +17,24 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
16
17
|
isConfigured() {
|
|
17
18
|
return !!this.apiKey;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if streaming is supported (always true for OpenAI)
|
|
22
|
+
*/
|
|
23
|
+
supportsStreaming() {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check multimodal support (vision models)
|
|
28
|
+
*/
|
|
29
|
+
supportsMultimodal() {
|
|
30
|
+
// Vision models support image input
|
|
31
|
+
const visionModels = ['gpt-4o', 'gpt-4-vision', 'gpt-4-turbo', 'gpt-4o-mini'];
|
|
32
|
+
const supportsVision = visionModels.some(m => this.model.includes(m));
|
|
33
|
+
return {
|
|
34
|
+
input: supportsVision ? ['text', 'image'] : ['text'],
|
|
35
|
+
output: ['text'] // DALL-E would be separate
|
|
36
|
+
};
|
|
37
|
+
}
|
|
19
38
|
async invoke(params) {
|
|
20
39
|
if (!this.isConfigured()) {
|
|
21
40
|
throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY environment variable.');
|
|
@@ -64,4 +83,157 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
64
83
|
} : undefined,
|
|
65
84
|
};
|
|
66
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Invoke with streaming response
|
|
88
|
+
*/
|
|
89
|
+
async invokeStream(params) {
|
|
90
|
+
if (!this.isConfigured()) {
|
|
91
|
+
throw new Error('OpenAI API key not configured. Set OPENAI_API_KEY environment variable.');
|
|
92
|
+
}
|
|
93
|
+
const url = `${this.baseUrl}/chat/completions`;
|
|
94
|
+
// Build messages with multimodal content if present
|
|
95
|
+
const messages = this.buildMessagesWithMedia(params);
|
|
96
|
+
const body = {
|
|
97
|
+
model: this.model,
|
|
98
|
+
messages,
|
|
99
|
+
temperature: params.temperature ?? 0.7,
|
|
100
|
+
max_tokens: params.maxTokens ?? 4096,
|
|
101
|
+
stream: true,
|
|
102
|
+
};
|
|
103
|
+
// Add JSON mode if schema provided
|
|
104
|
+
if (params.jsonSchema) {
|
|
105
|
+
body.response_format = { type: 'json_object' };
|
|
106
|
+
}
|
|
107
|
+
const response = await fetch(url, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/json',
|
|
111
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify(body),
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const error = await response.text();
|
|
117
|
+
throw new Error(`OpenAI API error: ${response.status} - ${error}`);
|
|
118
|
+
}
|
|
119
|
+
const bodyReader = response.body?.getReader();
|
|
120
|
+
if (!bodyReader) {
|
|
121
|
+
throw new Error('No response body');
|
|
122
|
+
}
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
let usage;
|
|
125
|
+
// Capture reader reference for closure
|
|
126
|
+
const reader = bodyReader;
|
|
127
|
+
// Create async generator for streaming
|
|
128
|
+
async function* streamGenerator() {
|
|
129
|
+
let buffer = '';
|
|
130
|
+
while (true) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done)
|
|
133
|
+
break;
|
|
134
|
+
buffer += decoder.decode(value, { stream: true });
|
|
135
|
+
// Parse SSE events
|
|
136
|
+
const lines = buffer.split('\n');
|
|
137
|
+
buffer = lines.pop() || '';
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
if (line.startsWith('data: ')) {
|
|
140
|
+
const data = line.slice(6);
|
|
141
|
+
if (data === '[DONE]') {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(data);
|
|
146
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
147
|
+
if (content) {
|
|
148
|
+
yield content;
|
|
149
|
+
}
|
|
150
|
+
// Capture usage if available
|
|
151
|
+
if (parsed.usage) {
|
|
152
|
+
usage = {
|
|
153
|
+
promptTokens: parsed.usage.prompt_tokens || 0,
|
|
154
|
+
completionTokens: parsed.usage.completion_tokens || 0,
|
|
155
|
+
totalTokens: parsed.usage.total_tokens || 0,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Skip malformed JSON
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
stream: streamGenerator(),
|
|
168
|
+
usage
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build messages with multimodal content (images)
|
|
173
|
+
*/
|
|
174
|
+
buildMessagesWithMedia(params) {
|
|
175
|
+
const hasImages = params.images && params.images.length > 0;
|
|
176
|
+
if (!hasImages) {
|
|
177
|
+
return params.messages;
|
|
178
|
+
}
|
|
179
|
+
// Find the last user message and add images to it
|
|
180
|
+
const messages = [];
|
|
181
|
+
const lastUserIdx = params.messages.findLastIndex(m => m.role === 'user');
|
|
182
|
+
for (let i = 0; i < params.messages.length; i++) {
|
|
183
|
+
const msg = params.messages[i];
|
|
184
|
+
if (i === lastUserIdx && hasImages) {
|
|
185
|
+
// Convert to multimodal content
|
|
186
|
+
const content = [
|
|
187
|
+
{ type: 'text', text: msg.content }
|
|
188
|
+
];
|
|
189
|
+
// Add images
|
|
190
|
+
for (const img of params.images) {
|
|
191
|
+
const imageUrl = this.mediaInputToUrl(img);
|
|
192
|
+
if (imageUrl) {
|
|
193
|
+
content.push({
|
|
194
|
+
type: 'image_url',
|
|
195
|
+
image_url: { url: imageUrl, detail: 'auto' }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
messages.push({ role: msg.role, content });
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
messages.push({ role: msg.role, content: msg.content });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Add JSON schema instruction if needed
|
|
206
|
+
if (params.jsonSchema && lastUserIdx >= 0) {
|
|
207
|
+
const lastMsg = messages[lastUserIdx];
|
|
208
|
+
if (typeof lastMsg.content === 'string') {
|
|
209
|
+
lastMsg.content = lastMsg.content + this.buildJsonPrompt(params.jsonSchema);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Content is array, append to text part
|
|
213
|
+
const textPart = lastMsg.content.find(p => p.type === 'text');
|
|
214
|
+
if (textPart && textPart.type === 'text') {
|
|
215
|
+
textPart.text = textPart.text + this.buildJsonPrompt(params.jsonSchema);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return messages;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Convert MediaInput to URL for OpenAI API
|
|
223
|
+
*/
|
|
224
|
+
mediaInputToUrl(media) {
|
|
225
|
+
switch (media.type) {
|
|
226
|
+
case 'url':
|
|
227
|
+
return media.url;
|
|
228
|
+
case 'base64':
|
|
229
|
+
return `data:${media.media_type};base64,${media.data}`;
|
|
230
|
+
case 'file':
|
|
231
|
+
// File paths would need to be loaded first
|
|
232
|
+
// This should be handled by the runner before calling the provider
|
|
233
|
+
console.warn('[cognitive] File media input not pre-loaded, skipping');
|
|
234
|
+
return null;
|
|
235
|
+
default:
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
67
239
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cognitive Modules HTTP API Server
|
|
3
|
+
*
|
|
4
|
+
* Provides RESTful API interface for workflow platform integration.
|
|
5
|
+
*
|
|
6
|
+
* Start with:
|
|
7
|
+
* cog serve --port 8000
|
|
8
|
+
*
|
|
9
|
+
* Environment variables:
|
|
10
|
+
* COGNITIVE_API_KEY - API Key authentication (optional)
|
|
11
|
+
* OPENAI_API_KEY, ANTHROPIC_API_KEY, etc. - LLM provider keys
|
|
12
|
+
*/
|
|
13
|
+
import http from 'node:http';
|
|
14
|
+
export interface ServeOptions {
|
|
15
|
+
host?: string;
|
|
16
|
+
port?: number;
|
|
17
|
+
cwd?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function createServer(options?: ServeOptions): http.Server;
|
|
20
|
+
export declare function serve(options?: ServeOptions): Promise<void>;
|