@kispace-io/extension-ai-system 0.8.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.
Files changed (56) hide show
  1. package/package.json +20 -0
  2. package/src/agents/agent-registry.ts +65 -0
  3. package/src/agents/index.ts +4 -0
  4. package/src/agents/message-processor.ts +50 -0
  5. package/src/agents/prompt-builder.ts +167 -0
  6. package/src/ai-system-extension.ts +104 -0
  7. package/src/aisystem.json +154 -0
  8. package/src/chat-provider-contributions.ts +95 -0
  9. package/src/core/constants.ts +23 -0
  10. package/src/core/index.ts +6 -0
  11. package/src/core/interfaces.ts +137 -0
  12. package/src/core/types.ts +126 -0
  13. package/src/general-assistant-prompt.txt +14 -0
  14. package/src/i18n.json +11 -0
  15. package/src/index.ts +13 -0
  16. package/src/prompt-enhancer-contributions.ts +29 -0
  17. package/src/providers/index.ts +5 -0
  18. package/src/providers/ollama-provider.ts +13 -0
  19. package/src/providers/openai-provider.ts +12 -0
  20. package/src/providers/provider-factory.ts +36 -0
  21. package/src/providers/provider.ts +156 -0
  22. package/src/providers/streaming/ollama-parser.ts +114 -0
  23. package/src/providers/streaming/sse-parser.ts +152 -0
  24. package/src/providers/streaming/stream-parser.ts +16 -0
  25. package/src/register.ts +16 -0
  26. package/src/service/ai-service.ts +744 -0
  27. package/src/service/token-usage-tracker.ts +139 -0
  28. package/src/tools/index.ts +4 -0
  29. package/src/tools/tool-call-accumulator.ts +81 -0
  30. package/src/tools/tool-executor.ts +174 -0
  31. package/src/tools/tool-registry.ts +70 -0
  32. package/src/translation.ts +3 -0
  33. package/src/utils/token-estimator.ts +87 -0
  34. package/src/utils/tool-detector.ts +144 -0
  35. package/src/view/agent-group-manager.ts +146 -0
  36. package/src/view/components/ai-agent-response-card.ts +198 -0
  37. package/src/view/components/ai-agent-response-group.ts +220 -0
  38. package/src/view/components/ai-chat-input.ts +131 -0
  39. package/src/view/components/ai-chat-message.ts +615 -0
  40. package/src/view/components/ai-empty-state.ts +52 -0
  41. package/src/view/components/ai-loading-indicator.ts +91 -0
  42. package/src/view/components/index.ts +7 -0
  43. package/src/view/components/k-ai-config-editor.ts +828 -0
  44. package/src/view/index.ts +6 -0
  45. package/src/view/k-aiview.ts +901 -0
  46. package/src/view/k-token-usage.ts +220 -0
  47. package/src/view/provider-manager.ts +196 -0
  48. package/src/view/session-manager.ts +255 -0
  49. package/src/view/stream-manager.ts +123 -0
  50. package/src/workflows/conditional-workflow.ts +98 -0
  51. package/src/workflows/index.ts +6 -0
  52. package/src/workflows/parallel-workflow.ts +45 -0
  53. package/src/workflows/sequential-workflow.ts +95 -0
  54. package/src/workflows/workflow-engine.ts +63 -0
  55. package/src/workflows/workflow-strategy.ts +21 -0
  56. package/tsconfig.json +12 -0
@@ -0,0 +1,139 @@
1
+ import { persistenceService } from "@kispace-io/core";
2
+ import type { TokenUsage, ProviderTokenUsage } from "../core/types";
3
+
4
+ interface TokenUsageData {
5
+ providers: Record<string, ProviderTokenUsage>;
6
+ total: ProviderTokenUsage;
7
+ lastUpdated: number;
8
+ }
9
+
10
+ const TOKEN_USAGE_KEY = "ai_token_usage";
11
+
12
+ export class TokenUsageTracker {
13
+ private data: TokenUsageData | null = null;
14
+ private loadPromise: Promise<TokenUsageData> | null = null;
15
+
16
+ private async loadData(): Promise<TokenUsageData> {
17
+ if (this.data) {
18
+ return this.data;
19
+ }
20
+
21
+ if (this.loadPromise) {
22
+ return this.loadPromise;
23
+ }
24
+
25
+ this.loadPromise = (async () => {
26
+ const stored = await persistenceService.getObject(TOKEN_USAGE_KEY);
27
+ if (stored) {
28
+ this.data = stored as TokenUsageData;
29
+ } else {
30
+ this.data = {
31
+ providers: {},
32
+ total: {
33
+ promptTokens: 0,
34
+ completionTokens: 0,
35
+ totalTokens: 0,
36
+ requestCount: 0
37
+ },
38
+ lastUpdated: Date.now()
39
+ };
40
+ await this.saveData();
41
+ }
42
+ this.loadPromise = null;
43
+ return this.data;
44
+ })();
45
+
46
+ return this.loadPromise;
47
+ }
48
+
49
+ private async saveData(): Promise<void> {
50
+ if (this.data) {
51
+ this.data.lastUpdated = Date.now();
52
+ await persistenceService.persistObject(TOKEN_USAGE_KEY, this.data);
53
+ }
54
+ }
55
+
56
+ async recordUsage(providerName: string, usage: TokenUsage): Promise<void> {
57
+ await this.loadData();
58
+
59
+ if (!this.data) {
60
+ return;
61
+ }
62
+
63
+ if (!this.data.providers[providerName]) {
64
+ this.data.providers[providerName] = {
65
+ promptTokens: 0,
66
+ completionTokens: 0,
67
+ totalTokens: 0,
68
+ requestCount: 0
69
+ };
70
+ }
71
+
72
+ const provider = this.data.providers[providerName];
73
+ provider.promptTokens += usage.promptTokens;
74
+ provider.completionTokens += usage.completionTokens;
75
+ provider.totalTokens += usage.totalTokens;
76
+ provider.requestCount += 1;
77
+
78
+ this.data.total.promptTokens += usage.promptTokens;
79
+ this.data.total.completionTokens += usage.completionTokens;
80
+ this.data.total.totalTokens += usage.totalTokens;
81
+ this.data.total.requestCount += 1;
82
+
83
+ await this.saveData();
84
+ }
85
+
86
+ async getProviderUsage(providerName: string): Promise<ProviderTokenUsage | null> {
87
+ await this.loadData();
88
+ return this.data?.providers[providerName] || null;
89
+ }
90
+
91
+ async getAllProviderUsage(): Promise<Record<string, ProviderTokenUsage>> {
92
+ await this.loadData();
93
+ return this.data?.providers || {};
94
+ }
95
+
96
+ async getTotalUsage(): Promise<ProviderTokenUsage> {
97
+ await this.loadData();
98
+ return this.data?.total || {
99
+ promptTokens: 0,
100
+ completionTokens: 0,
101
+ totalTokens: 0,
102
+ requestCount: 0
103
+ };
104
+ }
105
+
106
+ async reset(): Promise<void> {
107
+ this.data = {
108
+ providers: {},
109
+ total: {
110
+ promptTokens: 0,
111
+ completionTokens: 0,
112
+ totalTokens: 0,
113
+ requestCount: 0
114
+ },
115
+ lastUpdated: Date.now()
116
+ };
117
+ await this.saveData();
118
+ }
119
+
120
+ async resetProvider(providerName: string): Promise<void> {
121
+ await this.loadData();
122
+ if (!this.data) {
123
+ return;
124
+ }
125
+
126
+ const provider = this.data.providers[providerName];
127
+ if (provider) {
128
+ this.data.total.promptTokens -= provider.promptTokens;
129
+ this.data.total.completionTokens -= provider.completionTokens;
130
+ this.data.total.totalTokens -= provider.totalTokens;
131
+ this.data.total.requestCount -= provider.requestCount;
132
+ delete this.data.providers[providerName];
133
+ await this.saveData();
134
+ }
135
+ }
136
+ }
137
+
138
+ export const tokenUsageTracker = new TokenUsageTracker();
139
+
@@ -0,0 +1,4 @@
1
+ export * from "./tool-executor";
2
+ export * from "./tool-call-accumulator";
3
+ export * from "./tool-registry";
4
+
@@ -0,0 +1,81 @@
1
+ import type { ToolCall, StreamChunk } from "../core/types";
2
+
3
+ export class ToolCallAccumulator {
4
+ private accumulatedToolCalls = new Map<string, ToolCall>();
5
+ private toolCallIndexMap = new Map<number, string>();
6
+
7
+ processChunk(chunk: StreamChunk): void {
8
+ if (chunk.type === 'token' && chunk.toolCalls && chunk.toolCalls.length > 0) {
9
+ for (const toolCall of chunk.toolCalls) {
10
+ const callIndex = (toolCall as any)._index;
11
+ const callId = toolCall.id;
12
+
13
+ let existing: ToolCall | undefined;
14
+ let targetId: string;
15
+
16
+ if (callIndex !== undefined && this.toolCallIndexMap.has(callIndex)) {
17
+ targetId = this.toolCallIndexMap.get(callIndex)!;
18
+ existing = this.accumulatedToolCalls.get(targetId);
19
+ } else if (callId && this.accumulatedToolCalls.has(callId)) {
20
+ targetId = callId;
21
+ existing = this.accumulatedToolCalls.get(targetId);
22
+ } else {
23
+ targetId = callId || `call_${callIndex !== undefined ? callIndex : Date.now()}_${Math.random()}`;
24
+ existing = undefined;
25
+ }
26
+
27
+ if (existing) {
28
+ const existingArgs = existing.function.arguments || "";
29
+ const newArgs = toolCall.function.arguments || "";
30
+ const combinedArgs = existingArgs + newArgs;
31
+
32
+ this.accumulatedToolCalls.set(targetId, {
33
+ id: targetId,
34
+ type: toolCall.type || existing.type,
35
+ function: {
36
+ name: toolCall.function.name || existing.function.name,
37
+ arguments: combinedArgs
38
+ }
39
+ });
40
+
41
+ if (callIndex !== undefined && !this.toolCallIndexMap.has(callIndex)) {
42
+ this.toolCallIndexMap.set(callIndex, targetId);
43
+ }
44
+ } else {
45
+ this.accumulatedToolCalls.set(targetId, {
46
+ ...toolCall,
47
+ id: targetId
48
+ });
49
+
50
+ if (callIndex !== undefined) {
51
+ this.toolCallIndexMap.set(callIndex, targetId);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ getFinalToolCalls(): ToolCall[] {
59
+ return Array.from(this.accumulatedToolCalls.values())
60
+ .filter(tc => tc.function.name && tc.function.name.trim().length > 0)
61
+ .map(tc => {
62
+ let args = tc.function.arguments || "";
63
+ if (!args || args.trim() === "") {
64
+ args = "{}";
65
+ }
66
+ return {
67
+ ...tc,
68
+ function: {
69
+ ...tc.function,
70
+ arguments: args
71
+ }
72
+ };
73
+ });
74
+ }
75
+
76
+ reset(): void {
77
+ this.accumulatedToolCalls.clear();
78
+ this.toolCallIndexMap.clear();
79
+ }
80
+ }
81
+
@@ -0,0 +1,174 @@
1
+ import type { Command, ExecutionContext } from "@kispace-io/core";
2
+ import { commandRegistry } from "@kispace-io/core";
3
+ import type { ToolCall, ToolResult } from "../core/types";
4
+ import { ToolCallAccumulator } from "./tool-call-accumulator";
5
+
6
+ export class ToolExecutor {
7
+ private sanitizeFunctionName(name: string): string {
8
+ return name
9
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
10
+ .replace(/^[^a-zA-Z]/, 'cmd_$&')
11
+ .replace(/_+/g, '_')
12
+ .replace(/^_|_$/g, '');
13
+ }
14
+
15
+ findCommand(toolCall: ToolCall, context: ExecutionContext): Command | null {
16
+ const sanitizedFunctionName = toolCall.function.name;
17
+
18
+ // First try direct lookup (in case command ID matches sanitized name)
19
+ const directCommand = commandRegistry.getCommand(sanitizedFunctionName);
20
+ if (directCommand) {
21
+ return directCommand;
22
+ }
23
+
24
+ // If not found, search through all commands to find one whose sanitized ID matches
25
+ const allCommands = commandRegistry.listCommands();
26
+ for (const [commandId, command] of Object.entries(allCommands)) {
27
+ const sanitizedId = this.sanitizeFunctionName(commandId);
28
+ if (sanitizedId === sanitizedFunctionName) {
29
+ return command;
30
+ }
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ private parseArguments(argsStr: string): Record<string, any> {
37
+ if (!argsStr || argsStr.trim() === "" || argsStr === "{}") {
38
+ return {};
39
+ }
40
+
41
+ try {
42
+ const parsed = JSON.parse(argsStr);
43
+ return parsed && typeof parsed === 'object' ? parsed : {};
44
+ } catch (e) {
45
+ const errorMsg = e instanceof Error ? e.message : String(e);
46
+ console.error(`[ToolExecutor] Failed to parse arguments: ${argsStr} - ${errorMsg}`);
47
+ return {};
48
+ }
49
+ }
50
+
51
+ private sanitizeArguments(
52
+ args: Record<string, any>,
53
+ command: Command | null
54
+ ): Record<string, any> {
55
+ if (!command || !command.parameters || !args || typeof args !== 'object') {
56
+ return args || {};
57
+ }
58
+
59
+ const sanitizedArgs: Record<string, any> = {};
60
+ command.parameters.forEach(param => {
61
+ const sanitizedParamName = this.sanitizeFunctionName(param.name);
62
+ if (sanitizedParamName in args) {
63
+ sanitizedArgs[param.name] = args[sanitizedParamName];
64
+ }
65
+ });
66
+
67
+ return sanitizedArgs;
68
+ }
69
+
70
+ async executeToolCall(toolCall: ToolCall, context: ExecutionContext): Promise<ToolResult> {
71
+ try {
72
+ const command = this.findCommand(toolCall, context);
73
+ const commandId = command?.id || toolCall.function.name;
74
+
75
+ const argsStr = toolCall.function.arguments || "{}";
76
+ const args = this.parseArguments(argsStr);
77
+ const sanitizedArgs = this.sanitizeArguments(args, command);
78
+
79
+ const freshContext = commandRegistry.createExecutionContext(sanitizedArgs);
80
+ const execContext: ExecutionContext = {
81
+ ...context,
82
+ ...freshContext,
83
+ params: sanitizedArgs
84
+ };
85
+
86
+ let commandResult = await commandRegistry.execute(commandId, execContext);
87
+
88
+ const commandName = command?.name || commandId;
89
+ const resultMessage: any = {
90
+ success: true,
91
+ message: `Command "${commandName}" executed successfully`,
92
+ command: commandId
93
+ };
94
+
95
+ if (sanitizedArgs && typeof sanitizedArgs === 'object' && Object.keys(sanitizedArgs).length > 0) {
96
+ resultMessage.parameters = sanitizedArgs;
97
+ }
98
+
99
+ if (!!commandResult) {
100
+ let resolvedResult = commandResult;
101
+ if (resolvedResult instanceof Promise) {
102
+ resolvedResult = await resolvedResult;
103
+ }
104
+ resultMessage.result = resolvedResult;
105
+
106
+ if (command?.output && command.output.length > 0) {
107
+ const outputDescriptions = command.output.map(v => `${v.name}: ${v.description || v.type || 'value'}`).join(', ');
108
+ resultMessage.output = outputDescriptions;
109
+ }
110
+ }
111
+
112
+ return {
113
+ id: toolCall.id,
114
+ result: resultMessage
115
+ };
116
+ } catch (error) {
117
+ const errorMessage = error instanceof Error ? error.message : String(error);
118
+ let command: Command | null = null;
119
+ try {
120
+ command = this.findCommand(toolCall, context);
121
+ } catch {
122
+ command = null;
123
+ }
124
+ const commandName = command?.name || toolCall.function.name;
125
+
126
+ let detailedError = errorMessage;
127
+
128
+ if (errorMessage.includes('No handler found') || errorMessage.includes('No handlers registered')) {
129
+ detailedError = `Command "${commandName}" cannot be executed. ${errorMessage}. This usually means the command is not available in the current context (e.g., a map editor may not be open or active).`;
130
+ } else if (errorMessage.includes('not available') || errorMessage.includes('GsMapEditor')) {
131
+ detailedError = `Command "${commandName}" cannot be executed: ${errorMessage}. Please ensure the required editor or component is open and active.`;
132
+ }
133
+
134
+ return {
135
+ id: toolCall.id,
136
+ result: null,
137
+ error: detailedError
138
+ };
139
+ }
140
+ }
141
+
142
+ async executeToolCalls(
143
+ toolCalls: ToolCall[],
144
+ context: ExecutionContext
145
+ ): Promise<ToolResult[]> {
146
+ const results: ToolResult[] = [];
147
+ for (const toolCall of toolCalls) {
148
+ const result = await this.executeToolCall(toolCall, context);
149
+ results.push(result);
150
+ }
151
+ return results;
152
+ }
153
+
154
+ createToolCallAccumulator(): ToolCallAccumulator {
155
+ return new ToolCallAccumulator();
156
+ }
157
+
158
+ createToolCallSignature(toolCall: ToolCall): string {
159
+ const argsStr = toolCall.function.arguments || "{}";
160
+ let args: any = {};
161
+ try {
162
+ const parsed = JSON.parse(argsStr);
163
+ args = parsed && typeof parsed === 'object' ? parsed : {};
164
+ } catch (e) {
165
+ args = {};
166
+ }
167
+ const sortedArgs = args && typeof args === 'object' ? Object.keys(args).sort().reduce((acc, key) => {
168
+ acc[key] = args[key];
169
+ return acc;
170
+ }, {} as any) : {};
171
+ return `${toolCall.function.name}:${JSON.stringify(sortedArgs)}`;
172
+ }
173
+ }
174
+
@@ -0,0 +1,70 @@
1
+ import type { Command, ExecutionContext } from "@kispace-io/core";
2
+ import { commandRegistry } from "@kispace-io/core";
3
+ import type { ToolDefinition } from "../core/types";
4
+
5
+ export class ToolRegistry {
6
+ private sanitizeFunctionName(name: string): string {
7
+ return name
8
+ .replace(/[^a-zA-Z0-9_-]/g, '_')
9
+ .replace(/^[^a-zA-Z]/, 'cmd_$&')
10
+ .replace(/_+/g, '_')
11
+ .replace(/^_|_$/g, '');
12
+ }
13
+
14
+ commandToTool(command: Command, context: ExecutionContext): ToolDefinition {
15
+ const properties: Record<string, {
16
+ type: string;
17
+ description: string;
18
+ enum?: string[];
19
+ }> = {};
20
+ const required: string[] = [];
21
+
22
+ command.parameters?.forEach(param => {
23
+ const sanitizedParamName = this.sanitizeFunctionName(param.name);
24
+ properties[sanitizedParamName] = {
25
+ type: param.type || "string",
26
+ description: param.description,
27
+ ...(param.allowedValues && { enum: param.allowedValues })
28
+ };
29
+ if (param.required === true) {
30
+ required.push(sanitizedParamName);
31
+ }
32
+ });
33
+
34
+ const sanitizedFunctionName = this.sanitizeFunctionName(command.id);
35
+
36
+ const toolDef: ToolDefinition = {
37
+ type: "function",
38
+ function: {
39
+ name: sanitizedFunctionName,
40
+ description: command.description || command.name,
41
+ parameters: {
42
+ type: "object",
43
+ properties,
44
+ required
45
+ }
46
+ }
47
+ };
48
+
49
+ return toolDef;
50
+ }
51
+
52
+ getAvailableTools(
53
+ context: ExecutionContext,
54
+ commandFilter?: (command: Command, context: ExecutionContext) => boolean
55
+ ): ToolDefinition[] {
56
+ // list all commands without context as tools might modify the state of the application when executed
57
+ // this increases token usage but allows for more precise tool selection and execution
58
+ const availableCommands = commandRegistry.listCommands();
59
+ let commandsArray = Object.values(availableCommands) as Command[];
60
+
61
+ if (commandFilter) {
62
+ commandsArray = commandsArray.filter((cmd: Command) =>
63
+ commandFilter(cmd, context)
64
+ );
65
+ }
66
+
67
+ return commandsArray.map((cmd: Command) => this.commandToTool(cmd, context));
68
+ }
69
+ }
70
+
@@ -0,0 +1,3 @@
1
+ import { i18n } from "@kispace-io/core";
2
+
3
+ export const t = i18n('aisystem');
@@ -0,0 +1,87 @@
1
+ import type { ApiMessage, ToolDefinition, ToolCall } from "../core/types";
2
+
3
+ export class TokenEstimator {
4
+ private static readonly AVERAGE_CHARS_PER_TOKEN = 4;
5
+ private static readonly TOOL_DEFINITION_OVERHEAD = 50;
6
+ private static readonly MESSAGE_OVERHEAD = 4;
7
+
8
+ static estimateTokens(text: string): number {
9
+ if (!text || text.trim().length === 0) {
10
+ return 0;
11
+ }
12
+
13
+ const trimmed = text.trim();
14
+ const charCount = trimmed.length;
15
+ const wordCount = trimmed.split(/\s+/).filter(w => w.length > 0).length;
16
+
17
+ const tokenEstimate = Math.ceil(
18
+ charCount / this.AVERAGE_CHARS_PER_TOKEN + wordCount * 0.3
19
+ );
20
+
21
+ return Math.max(1, tokenEstimate);
22
+ }
23
+
24
+ static estimateMessageTokens(message: ApiMessage): number {
25
+ let tokens = this.MESSAGE_OVERHEAD;
26
+
27
+ if (message.content) {
28
+ tokens += this.estimateTokens(message.content);
29
+ }
30
+
31
+ if (message.role) {
32
+ tokens += this.estimateTokens(message.role);
33
+ }
34
+
35
+ if (message.tool_calls) {
36
+ for (const toolCall of message.tool_calls) {
37
+ tokens += this.estimateTokens(toolCall.function.name || '');
38
+ tokens += this.estimateTokens(toolCall.function.arguments || '{}');
39
+ tokens += 10;
40
+ }
41
+ }
42
+
43
+ if (message.tool_call_id) {
44
+ tokens += this.estimateTokens(message.tool_call_id);
45
+ }
46
+
47
+ return tokens;
48
+ }
49
+
50
+ static estimatePromptTokens(messages: ApiMessage[], tools?: ToolDefinition[]): number {
51
+ let totalTokens = 0;
52
+
53
+ for (const message of messages) {
54
+ totalTokens += this.estimateMessageTokens(message);
55
+ }
56
+
57
+ if (tools && tools.length > 0) {
58
+ for (const tool of tools) {
59
+ totalTokens += this.TOOL_DEFINITION_OVERHEAD;
60
+ totalTokens += this.estimateTokens(tool.function.name || '');
61
+ totalTokens += this.estimateTokens(tool.function.description || '');
62
+
63
+ if (tool.function.parameters) {
64
+ const paramsJson = JSON.stringify(tool.function.parameters);
65
+ totalTokens += this.estimateTokens(paramsJson);
66
+ }
67
+ }
68
+ }
69
+
70
+ return totalTokens;
71
+ }
72
+
73
+ static estimateCompletionTokens(content: string, toolCalls?: ToolCall[]): number {
74
+ let tokens = this.estimateTokens(content);
75
+
76
+ if (toolCalls && toolCalls.length > 0) {
77
+ for (const toolCall of toolCalls) {
78
+ tokens += 10;
79
+ tokens += this.estimateTokens(toolCall.function?.name || '');
80
+ tokens += this.estimateTokens(toolCall.function?.arguments || '{}');
81
+ }
82
+ }
83
+
84
+ return tokens;
85
+ }
86
+ }
87
+