@axiom-lattice/gateway 2.1.11 → 2.1.13

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/src/config.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Configuration service
3
+ * Manages environment variables and supports dynamic updates
4
+ */
5
+
6
+ export interface GatewayConfig {
7
+ port?: number;
8
+ queueServiceType?: string;
9
+ redisUrl?: string;
10
+ redisPassword?: string;
11
+ queueName?: string;
12
+ [key: string]: any; // Allow additional config keys
13
+ }
14
+
15
+ /**
16
+ * Get configuration from environment variables
17
+ * Supports dynamic updates via updateConfig method
18
+ */
19
+ class ConfigService {
20
+ private config: GatewayConfig;
21
+
22
+ constructor() {
23
+ this.config = this.loadFromEnv();
24
+ }
25
+
26
+ /**
27
+ * Load configuration from environment variables
28
+ */
29
+ private loadFromEnv(): GatewayConfig {
30
+ return {
31
+ port: process.env.PORT ? Number(process.env.PORT) : undefined,
32
+ queueServiceType: process.env.QUEUE_SERVICE_TYPE,
33
+ redisUrl: process.env.REDIS_URL,
34
+ redisPassword: process.env.REDIS_PASSWORD,
35
+ queueName: process.env.QUEUE_NAME,
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Update configuration from JSON object
41
+ * This will update both the internal config and process.env
42
+ */
43
+ updateConfig(jsonConfig: Record<string, any>): void {
44
+ // Update process.env for all provided keys
45
+ for (const [key, value] of Object.entries(jsonConfig)) {
46
+ if (value !== null && value !== undefined) {
47
+ // Convert nested objects to environment variable format
48
+ if (typeof value === "object" && !Array.isArray(value)) {
49
+ // Handle nested objects like supabase: { url: "...", key: "..." }
50
+ for (const [nestedKey, nestedValue] of Object.entries(value)) {
51
+ const envKey = `${key.toUpperCase()}_${nestedKey.toUpperCase()}`;
52
+ process.env[envKey] = String(nestedValue);
53
+ }
54
+ } else {
55
+ // Handle flat keys
56
+ process.env[key.toUpperCase()] = String(value);
57
+ }
58
+ }
59
+ }
60
+
61
+ // Reload config from updated environment variables
62
+ this.config = this.loadFromEnv();
63
+
64
+ // Deep merge the JSON config into our config object
65
+ this.config = this.deepMerge(this.config, jsonConfig);
66
+ }
67
+
68
+ /**
69
+ * Deep merge two objects
70
+ */
71
+ private deepMerge(target: any, source: any): any {
72
+ const output = { ...target };
73
+ if (this.isObject(target) && this.isObject(source)) {
74
+ Object.keys(source).forEach((key) => {
75
+ if (this.isObject(source[key])) {
76
+ if (!(key in target)) {
77
+ Object.assign(output, { [key]: source[key] });
78
+ } else {
79
+ output[key] = this.deepMerge(target[key], source[key]);
80
+ }
81
+ } else {
82
+ Object.assign(output, { [key]: source[key] });
83
+ }
84
+ });
85
+ }
86
+ return output;
87
+ }
88
+
89
+ /**
90
+ * Check if value is a plain object
91
+ */
92
+ private isObject(item: any): boolean {
93
+ return item && typeof item === "object" && !Array.isArray(item);
94
+ }
95
+
96
+ /**
97
+ * Get current configuration
98
+ */
99
+ getConfig(): GatewayConfig {
100
+ return { ...this.config };
101
+ }
102
+ }
103
+
104
+ // Export singleton instance
105
+ export const configService = new ConfigService();
106
+
107
+ // Export config getter for backward compatibility
108
+ export const config = {
109
+ get port() {
110
+ return configService.getConfig().port;
111
+ },
112
+ get queueServiceType() {
113
+ return configService.getConfig().queueServiceType;
114
+ },
115
+ get redisUrl() {
116
+ return configService.getConfig().redisUrl;
117
+ },
118
+ get redisPassword() {
119
+ return configService.getConfig().redisPassword;
120
+ },
121
+ get queueName() {
122
+ return configService.getConfig().queueName;
123
+ },
124
+ };
@@ -1,6 +1,277 @@
1
1
  import { FastifyRequest, FastifyReply } from "fastify";
2
2
  import { draw_graph } from "../services/agent_service";
3
+ import { assistantStore } from "../stores/assistant_store";
4
+ import { Assistant, CreateAssistantRequest } from "../types";
5
+ import { randomUUID } from "crypto";
6
+ import { AgentConfig, getAllAgentConfigs } from "@axiom-lattice/core";
3
7
 
8
+ /**
9
+ * Assistant Controller
10
+ * Handles assistant-related CRUD operations
11
+ * Merges code-configured agents (from @axiom-lattice/core) with in-memory stored assistants
12
+ * GET operations return both code-configured and stored assistants
13
+ * CUD operations only work on stored assistants
14
+ */
15
+
16
+ /**
17
+ * Convert AgentConfig to Assistant format
18
+ */
19
+ function convertAgentConfigToAssistant(config: AgentConfig): Assistant {
20
+ return {
21
+ id: config.key,
22
+ name: config.name,
23
+ description: config.description,
24
+ graphDefinition: config, // Store the full config as graphDefinition
25
+ createdAt: new Date(0), // Code-configured agents have no creation date
26
+ updatedAt: new Date(0), // Code-configured agents have no update date
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Assistant list response interface
32
+ */
33
+ interface AssistantListResponse {
34
+ success: boolean;
35
+ message: string;
36
+ data: {
37
+ records: Assistant[];
38
+ total: number;
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Assistant response interface
44
+ */
45
+ interface AssistantResponse {
46
+ success: boolean;
47
+ message: string;
48
+ data?: Assistant;
49
+ }
50
+
51
+ /**
52
+ * Assistant update request body interface
53
+ */
54
+ interface AssistantUpdateBody {
55
+ name?: string;
56
+ description?: string;
57
+ graphDefinition?: any;
58
+ }
59
+
60
+ /**
61
+ * Get list of all assistants
62
+ * Merges code-configured agents with stored assistants
63
+ */
64
+ export async function getAssistantList(
65
+ request: FastifyRequest,
66
+ reply: FastifyReply
67
+ ): Promise<AssistantListResponse> {
68
+ // Get code-configured agents
69
+ const agentConfigs = await getAllAgentConfigs();
70
+ const codeConfiguredAssistants = agentConfigs.map(
71
+ convertAgentConfigToAssistant
72
+ );
73
+
74
+ // Get stored assistants
75
+ const storedAssistants = assistantStore.getAllAssistants();
76
+
77
+ // Merge both sources, stored assistants take precedence if ID conflicts
78
+ const assistantMap = new Map<string, Assistant>();
79
+
80
+ // First add code-configured agents
81
+ codeConfiguredAssistants.forEach((assistant) => {
82
+ assistantMap.set(assistant.id, assistant);
83
+ });
84
+
85
+ // Then add stored assistants (overwrite if ID exists)
86
+ storedAssistants.forEach((assistant) => {
87
+ assistantMap.set(assistant.id, assistant);
88
+ });
89
+
90
+ const allAssistants = Array.from(assistantMap.values());
91
+
92
+ return {
93
+ success: true,
94
+ message: "Successfully retrieved assistant list",
95
+ data: {
96
+ records: allAssistants,
97
+ total: allAssistants.length,
98
+ },
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Get a single assistant by ID
104
+ * Checks both code-configured agents and stored assistants
105
+ */
106
+ export async function getAssistant(
107
+ request: FastifyRequest<{ Params: { id: string } }>,
108
+ reply: FastifyReply
109
+ ): Promise<AssistantResponse> {
110
+ const { id } = request.params;
111
+
112
+ // First check stored assistants
113
+ let assistant = assistantStore.getAssistantById(id);
114
+
115
+ // If not found in store, check code-configured agents
116
+ if (!assistant) {
117
+ const agentConfigs = await getAllAgentConfigs();
118
+ const agentConfig = agentConfigs.find((config) => config.key === id);
119
+ if (agentConfig) {
120
+ assistant = convertAgentConfigToAssistant(agentConfig);
121
+ }
122
+ }
123
+
124
+ if (!assistant) {
125
+ return reply.status(404).send({
126
+ success: false,
127
+ message: "Assistant not found",
128
+ });
129
+ }
130
+
131
+ return {
132
+ success: true,
133
+ message: "Successfully retrieved assistant",
134
+ data: assistant,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Create a new assistant
140
+ */
141
+ export async function createAssistant(
142
+ request: FastifyRequest<{ Body: CreateAssistantRequest }>,
143
+ reply: FastifyReply
144
+ ): Promise<AssistantResponse> {
145
+ const data = request.body;
146
+
147
+ // Validate required fields
148
+ if (!data.name) {
149
+ return reply.status(400).send({
150
+ success: false,
151
+ message: "name is required",
152
+ });
153
+ }
154
+
155
+ if (!data.graphDefinition) {
156
+ return reply.status(400).send({
157
+ success: false,
158
+ message: "graphDefinition is required",
159
+ });
160
+ }
161
+
162
+ // Generate ID if not provided
163
+ const id = randomUUID();
164
+
165
+ // Create assistant
166
+ const newAssistant = assistantStore.createAssistant(id, data);
167
+
168
+ return reply.status(201).send({
169
+ success: true,
170
+ message: "Successfully created assistant",
171
+ data: newAssistant,
172
+ });
173
+ }
174
+
175
+ /**
176
+ * Update an existing assistant by ID
177
+ * Only works on stored assistants, not code-configured ones
178
+ */
179
+ export async function updateAssistant(
180
+ request: FastifyRequest<{
181
+ Params: { id: string };
182
+ Body: AssistantUpdateBody;
183
+ }>,
184
+ reply: FastifyReply
185
+ ): Promise<AssistantResponse> {
186
+ const { id } = request.params;
187
+ const updates = request.body;
188
+
189
+ // Check if it's a code-configured agent
190
+ const agentConfigs = await getAllAgentConfigs();
191
+ const isCodeConfigured = agentConfigs.some((config) => config.key === id);
192
+
193
+ if (isCodeConfigured) {
194
+ return reply.status(403).send({
195
+ success: false,
196
+ message:
197
+ "Cannot update code-configured assistant. Only stored assistants can be updated.",
198
+ });
199
+ }
200
+
201
+ // Check if assistant exists in store
202
+ if (!assistantStore.hasAssistant(id)) {
203
+ return reply.status(404).send({
204
+ success: false,
205
+ message: "Assistant not found",
206
+ });
207
+ }
208
+
209
+ // Update assistant
210
+ const updatedAssistant = assistantStore.updateAssistant(id, updates);
211
+
212
+ if (!updatedAssistant) {
213
+ return reply.status(500).send({
214
+ success: false,
215
+ message: "Failed to update assistant",
216
+ });
217
+ }
218
+
219
+ return {
220
+ success: true,
221
+ message: "Successfully updated assistant",
222
+ data: updatedAssistant,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Delete an assistant by ID
228
+ * Only works on stored assistants, not code-configured ones
229
+ */
230
+ export async function deleteAssistant(
231
+ request: FastifyRequest<{ Params: { id: string } }>,
232
+ reply: FastifyReply
233
+ ): Promise<{ success: boolean; message: string }> {
234
+ const { id } = request.params;
235
+
236
+ // Check if it's a code-configured agent
237
+ const agentConfigs = await getAllAgentConfigs();
238
+ const isCodeConfigured = agentConfigs.some((config) => config.key === id);
239
+
240
+ if (isCodeConfigured) {
241
+ return reply.status(403).send({
242
+ success: false,
243
+ message:
244
+ "Cannot delete code-configured assistant. Only stored assistants can be deleted.",
245
+ });
246
+ }
247
+
248
+ // Check if assistant exists in store
249
+ if (!assistantStore.hasAssistant(id)) {
250
+ return reply.status(404).send({
251
+ success: false,
252
+ message: "Assistant not found",
253
+ });
254
+ }
255
+
256
+ // Delete the assistant
257
+ const deleted = assistantStore.deleteAssistant(id);
258
+
259
+ if (!deleted) {
260
+ return reply.status(500).send({
261
+ success: false,
262
+ message: "Failed to delete assistant",
263
+ });
264
+ }
265
+
266
+ return {
267
+ success: true,
268
+ message: "Successfully deleted assistant",
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Get agent graph visualization
274
+ */
4
275
  export const getAgentGraph = async (
5
276
  request: FastifyRequest<{
6
277
  Params: { assistantId: string };
@@ -10,17 +281,17 @@ export const getAgentGraph = async (
10
281
  try {
11
282
  const { assistantId } = request.params;
12
283
 
13
- // 调用绘图服务获取图片数据
284
+ // Call drawing service to get image data
14
285
  const imageData = await draw_graph(assistantId);
15
286
 
16
- // 设置响应头并返回图片数据
287
+ // Set response header and return image data
17
288
  reply.header("Content-Type", "application/json").send({
18
289
  image: imageData,
19
290
  });
20
291
  } catch (error: any) {
21
292
  reply.status(500).send({
22
293
  success: false,
23
- error: error.message || "获取代理图表失败",
294
+ error: error.message || "Failed to get agent graph",
24
295
  });
25
296
  }
26
297
  };
@@ -0,0 +1,126 @@
1
+ import { FastifyRequest, FastifyReply } from "fastify";
2
+ import { configService } from "../config";
3
+ import {
4
+ setQueueServiceType,
5
+ QueueServiceType,
6
+ } from "../services/queue_service";
7
+
8
+ /**
9
+ * Configuration Controller
10
+ * Handles configuration updates from frontend
11
+ */
12
+
13
+ interface UpdateConfigRequest {
14
+ Body: {
15
+ config: Record<string, any>;
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Update gateway configuration
21
+ * Accepts JSON config and loads it into environment variables
22
+ */
23
+ export async function updateConfig(
24
+ request: FastifyRequest<UpdateConfigRequest>,
25
+ reply: FastifyReply
26
+ ) {
27
+ try {
28
+ const { config: jsonConfig } = request.body;
29
+
30
+ if (!jsonConfig || typeof jsonConfig !== "object") {
31
+ return reply.status(400).send({
32
+ success: false,
33
+ error: "Invalid configuration: config must be an object",
34
+ });
35
+ }
36
+
37
+ // Update configuration service
38
+ configService.updateConfig(jsonConfig);
39
+
40
+ const warnings: string[] = [];
41
+ const requiresRestart: string[] = [];
42
+
43
+ // Check if port is being changed (requires restart)
44
+ if (jsonConfig.port !== undefined) {
45
+ requiresRestart.push("PORT");
46
+ warnings.push("Port change requires server restart to take effect");
47
+ }
48
+
49
+ // If queue service type is being updated, reconfigure the queue service
50
+ if (jsonConfig.queueServiceType) {
51
+ setQueueServiceType(jsonConfig.queueServiceType as QueueServiceType);
52
+ }
53
+
54
+ // If Redis configuration is being updated and queue service is Redis, reconfigure
55
+ if (
56
+ (jsonConfig.redisUrl || jsonConfig.redisPassword) &&
57
+ (process.env.QUEUE_SERVICE_TYPE === "redis" ||
58
+ jsonConfig.queueServiceType === "redis")
59
+ ) {
60
+ // Reconfigure queue service to pick up new Redis settings
61
+ const currentType =
62
+ (jsonConfig.queueServiceType as QueueServiceType) ||
63
+ (process.env.QUEUE_SERVICE_TYPE as QueueServiceType) ||
64
+ "memory";
65
+ if (currentType === "redis") {
66
+ setQueueServiceType("redis");
67
+ }
68
+ }
69
+
70
+ // Get updated config (without sensitive data)
71
+ const updatedConfig = configService.getConfig();
72
+ const safeConfig = {
73
+ ...updatedConfig,
74
+ redisPassword: updatedConfig.redisPassword
75
+ ? "***"
76
+ : updatedConfig.redisPassword,
77
+ };
78
+
79
+ return reply.send({
80
+ success: true,
81
+ message: "Configuration updated successfully",
82
+ data: safeConfig,
83
+ warnings: warnings.length > 0 ? warnings : undefined,
84
+ requiresRestart: requiresRestart.length > 0 ? requiresRestart : undefined,
85
+ });
86
+ } catch (error: any) {
87
+ console.error("Failed to update configuration", {
88
+ error: error.message,
89
+ stack: error.stack,
90
+ });
91
+ return reply.status(500).send({
92
+ success: false,
93
+ error: error.message || "Failed to update configuration",
94
+ });
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get current gateway configuration
100
+ * Returns current configuration (with sensitive data masked)
101
+ */
102
+ export async function getConfig(request: FastifyRequest, reply: FastifyReply) {
103
+ try {
104
+ const currentConfig = configService.getConfig();
105
+ const safeConfig = {
106
+ ...currentConfig,
107
+ redisPassword: currentConfig.redisPassword
108
+ ? "***"
109
+ : currentConfig.redisPassword,
110
+ };
111
+
112
+ return reply.send({
113
+ success: true,
114
+ data: safeConfig,
115
+ });
116
+ } catch (error: any) {
117
+ console.error("Failed to get configuration", {
118
+ error: error.message,
119
+ stack: error.stack,
120
+ });
121
+ return reply.status(500).send({
122
+ success: false,
123
+ error: error.message || "Failed to get configuration",
124
+ });
125
+ }
126
+ }
@@ -0,0 +1,152 @@
1
+ import { FastifyRequest, FastifyReply } from "fastify";
2
+ import { registerModelLattice, modelLatticeManager } from "@axiom-lattice/core";
3
+ import type { LLMConfig } from "@axiom-lattice/protocols";
4
+
5
+ /**
6
+ * Models Controller
7
+ * Handles model lattice registration and management
8
+ */
9
+
10
+ interface ModelConfig {
11
+ key: string;
12
+ model: string;
13
+ provider: "azure" | "openai" | "deepseek" | "siliconcloud" | "volcengine";
14
+ streaming?: boolean;
15
+ apiKey?: string;
16
+ baseURL?: string;
17
+ maxTokens?: number;
18
+ temperature?: number;
19
+ timeout?: number;
20
+ maxRetries?: number;
21
+ }
22
+
23
+ interface UpdateModelsRequest {
24
+ Body: {
25
+ models: ModelConfig[];
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Get all registered models
31
+ */
32
+ export async function getModels(request: FastifyRequest, reply: FastifyReply) {
33
+ try {
34
+ const allLattices = modelLatticeManager.getAllLattices();
35
+ const models = allLattices.map((lattice) => {
36
+ // Extract config from the lattice client
37
+ // Note: This is a simplified approach - you may need to adjust based on actual implementation
38
+ const config = (lattice.client as any).config || {};
39
+ return {
40
+ key: lattice.key,
41
+ model: config.model || "",
42
+ provider: config.provider || "openai",
43
+ streaming: config.streaming || false,
44
+ apiKey: config.apiKey || "",
45
+ baseURL: config.baseURL || "",
46
+ maxTokens: config.maxTokens,
47
+ temperature: config.temperature,
48
+ timeout: config.timeout,
49
+ maxRetries: config.maxRetries,
50
+ };
51
+ });
52
+
53
+ return reply.send({
54
+ success: true,
55
+ data: models,
56
+ });
57
+ } catch (error: any) {
58
+ console.error("Failed to get models", {
59
+ error: error.message,
60
+ stack: error.stack,
61
+ });
62
+ return reply.status(500).send({
63
+ success: false,
64
+ error: error.message || "Failed to get models",
65
+ });
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Update models configuration
71
+ * Registers or updates model lattices
72
+ */
73
+ export async function updateModels(
74
+ request: FastifyRequest<UpdateModelsRequest>,
75
+ reply: FastifyReply
76
+ ) {
77
+ try {
78
+ const { models } = request.body;
79
+
80
+ if (!models || !Array.isArray(models)) {
81
+ return reply.status(400).send({
82
+ success: false,
83
+ error: "Invalid request: models must be an array",
84
+ });
85
+ }
86
+
87
+ const registeredModels: string[] = [];
88
+ const errors: string[] = [];
89
+
90
+ for (const modelConfig of models) {
91
+ if (!modelConfig.key || !modelConfig.model || !modelConfig.provider) {
92
+ errors.push(
93
+ `Model configuration is incomplete: key, model, and provider are required`
94
+ );
95
+ continue;
96
+ }
97
+
98
+ try {
99
+ // Remove existing model with the same key if it exists
100
+ if (modelLatticeManager.hasLattice(modelConfig.key)) {
101
+ modelLatticeManager.removeLattice(modelConfig.key);
102
+ }
103
+
104
+ // Convert to LLMConfig format
105
+ const llmConfig: LLMConfig = {
106
+ provider: modelConfig.provider,
107
+ model: modelConfig.model,
108
+ streaming: modelConfig.streaming ?? false,
109
+ apiKey: modelConfig.apiKey,
110
+ baseURL: modelConfig.baseURL,
111
+ maxTokens: modelConfig.maxTokens,
112
+ temperature: modelConfig.temperature,
113
+ timeout: modelConfig.timeout,
114
+ maxRetries: modelConfig.maxRetries,
115
+ };
116
+
117
+ // Register the new model lattice
118
+ registerModelLattice(modelConfig.key, llmConfig);
119
+ registeredModels.push(modelConfig.key);
120
+ } catch (error: any) {
121
+ errors.push(
122
+ `Failed to register model ${modelConfig.key}: ${error.message}`
123
+ );
124
+ }
125
+ }
126
+
127
+ if (errors.length > 0 && registeredModels.length === 0) {
128
+ return reply.status(400).send({
129
+ success: false,
130
+ error: errors.join("; "),
131
+ });
132
+ }
133
+
134
+ return reply.send({
135
+ success: true,
136
+ message: `Successfully registered ${registeredModels.length} model(s)`,
137
+ data: {
138
+ registered: registeredModels,
139
+ errors: errors.length > 0 ? errors : undefined,
140
+ },
141
+ });
142
+ } catch (error: any) {
143
+ console.error("Failed to update models", {
144
+ error: error.message,
145
+ stack: error.stack,
146
+ });
147
+ return reply.status(500).send({
148
+ success: false,
149
+ error: error.message || "Failed to update models",
150
+ });
151
+ }
152
+ }