feat(agent): inject userId into system prompt + fix agent-instance nullable columns
- SystemPromptBuilder: add userId/userEmail to context, expose internal API curl commands for OpenClaw creation - agent.controller.ts: extract userId from JWT, build system prompt via SystemPromptBuilder so iAgent knows current user - agent.module.ts: register SystemPromptBuilder as provider - agent-instance.entity.ts: make serverHost/sshUser nullable (pool mode doesn't set these upfront) - DB: ALTER TABLE agent_instances DROP NOT NULL on server_host/ssh_user Now iAgent can create 小龙虾 instances autonomously when user asks in natural language. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
49e48d7b3e
commit
b87cebf465
|
|
@ -52,6 +52,7 @@ import { AgentInstance } from './domain/entities/agent-instance.entity';
|
||||||
import { AgentInstanceRepository } from './infrastructure/repositories/agent-instance.repository';
|
import { AgentInstanceRepository } from './infrastructure/repositories/agent-instance.repository';
|
||||||
import { AgentInstanceDeployService } from './infrastructure/services/agent-instance-deploy.service';
|
import { AgentInstanceDeployService } from './infrastructure/services/agent-instance-deploy.service';
|
||||||
import { AgentInstanceController } from './interfaces/rest/controllers/agent-instance.controller';
|
import { AgentInstanceController } from './interfaces/rest/controllers/agent-instance.controller';
|
||||||
|
import { SystemPromptBuilder } from './infrastructure/engines/claude-code-cli/system-prompt-builder';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -99,6 +100,7 @@ import { AgentInstanceController } from './interfaces/rest/controllers/agent-ins
|
||||||
OpenAISttService,
|
OpenAISttService,
|
||||||
AgentInstanceRepository,
|
AgentInstanceRepository,
|
||||||
AgentInstanceDeployService,
|
AgentInstanceDeployService,
|
||||||
|
SystemPromptBuilder,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AgentModule {}
|
export class AgentModule {}
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,14 @@ export class AgentInstance {
|
||||||
@Column({ type: 'uuid', name: 'pool_server_id', nullable: true })
|
@Column({ type: 'uuid', name: 'pool_server_id', nullable: true })
|
||||||
poolServerId?: string;
|
poolServerId?: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 255, name: 'server_host' })
|
@Column({ type: 'varchar', length: 255, name: 'server_host', nullable: true })
|
||||||
serverHost!: string;
|
serverHost?: string;
|
||||||
|
|
||||||
@Column({ type: 'int', name: 'ssh_port', default: 22 })
|
@Column({ type: 'int', name: 'ssh_port', default: 22 })
|
||||||
sshPort!: number;
|
sshPort!: number;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 100, name: 'ssh_user' })
|
@Column({ type: 'varchar', length: 100, name: 'ssh_user', nullable: true })
|
||||||
sshUser!: string;
|
sshUser?: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 150, name: 'container_name', unique: true })
|
@Column({ type: 'varchar', length: 150, name: 'container_name', unique: true })
|
||||||
containerName!: string;
|
containerName!: string;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import * as path from 'path';
|
||||||
|
|
||||||
export interface SystemPromptContext {
|
export interface SystemPromptContext {
|
||||||
tenantId: string;
|
tenantId: string;
|
||||||
|
userId?: string;
|
||||||
|
userEmail?: string;
|
||||||
serverContext?: string;
|
serverContext?: string;
|
||||||
skills?: string[];
|
skills?: string[];
|
||||||
riskBoundary?: string;
|
riskBoundary?: string;
|
||||||
|
|
@ -33,21 +35,24 @@ export class SystemPromptBuilder {
|
||||||
'You are iAgent, an AI-powered server cluster operations assistant built on IT0. ' +
|
'You are iAgent, an AI-powered server cluster operations assistant built on IT0. ' +
|
||||||
'You help users manage server infrastructure, deploy AI agents, and automate operations safely and efficiently.\n\n' +
|
'You help users manage server infrastructure, deploy AI agents, and automate operations safely and efficiently.\n\n' +
|
||||||
'## OpenClaw Agent Deployment\n' +
|
'## OpenClaw Agent Deployment\n' +
|
||||||
'You can deploy real OpenClaw AI agent instances for users. OpenClaw is an open-source autonomous AI agent.\n' +
|
'You can deploy real OpenClaw (小龙虾) AI agent instances for users. OpenClaw is an open-source autonomous AI agent.\n' +
|
||||||
'To deploy an OpenClaw instance, call the IT0 API:\n' +
|
'Use the internal API (no auth required, direct service call):\n' +
|
||||||
' POST /api/v1/agent/instances\n' +
|
' Create: curl -s -X POST http://localhost:3002/api/v1/agent/instances -H "Content-Type: application/json" -d \'{"name":"<name>","userId":"<userId>","usePool":true}\'\n' +
|
||||||
' Body: { "name": "<user-given name>", "userId": "<user id>", "usePool": true }\n' +
|
' List: curl -s http://localhost:3002/api/v1/agent/instances\n' +
|
||||||
'The instance will be deployed on a pool server automatically. After creation, the user can connect\n' +
|
' Stop: curl -s -X PUT http://localhost:3002/api/v1/agent/instances/<id>/stop\n' +
|
||||||
'their OpenClaw to Telegram, WhatsApp, or other channels via the channel configuration.\n' +
|
' Delete: curl -s -X DELETE http://localhost:3002/api/v1/agent/instances/<id>\n\n' +
|
||||||
'To list existing instances: GET /api/v1/agent/instances\n' +
|
'When a user asks to create a OpenClaw / 小龙虾 / personal AI agent / autonomous agent:\n' +
|
||||||
'To stop an instance: PUT /api/v1/agent/instances/:id/stop\n' +
|
'1. Ask for a name if not given\n' +
|
||||||
'To remove an instance: DELETE /api/v1/agent/instances/:id\n\n' +
|
'2. Use the Current User ID from this prompt as userId\n' +
|
||||||
'When a user says they want to create an OpenClaw, a personal AI agent, or an autonomous agent,\n' +
|
'3. Call the create API with Bash and report the result (id, status, containerName)',
|
||||||
'confirm their desired name, then call the deployment API and report the result.',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tenant context
|
// Tenant + user context
|
||||||
parts.push(`\nTenant: ${context.tenantId}`);
|
parts.push(`\nTenant: ${context.tenantId}`);
|
||||||
|
if (context.userId) {
|
||||||
|
parts.push(`Current User ID: ${context.userId}${context.userEmail ? ` (${context.userEmail})` : ''}`);
|
||||||
|
parts.push(`When creating OpenClaw instances for the current user, use userId: "${context.userId}"`);
|
||||||
|
}
|
||||||
|
|
||||||
// Server context (available servers, environment, etc.)
|
// Server context (available servers, environment, etc.)
|
||||||
if (context.serverContext) {
|
if (context.serverContext) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Controller, Post, Body, Param, Delete, Get, NotFoundException, BadRequestException, ForbiddenException, Logger, UseInterceptors, UploadedFile } from '@nestjs/common';
|
import { Controller, Post, Body, Param, Delete, Get, NotFoundException, BadRequestException, ForbiddenException, Logger, UseInterceptors, UploadedFile, Req } from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { memoryStorage } from 'multer';
|
import { memoryStorage } from 'multer';
|
||||||
import { TenantId, EventPatterns } from '@it0/common';
|
import { TenantId, EventPatterns } from '@it0/common';
|
||||||
|
|
@ -17,6 +17,7 @@ import { TaskStatus } from '../../../domain/value-objects/task-status.vo';
|
||||||
import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo';
|
import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo';
|
||||||
import { AgentEnginePort, EngineStreamEvent } from '../../../domain/ports/outbound/agent-engine.port';
|
import { AgentEnginePort, EngineStreamEvent } from '../../../domain/ports/outbound/agent-engine.port';
|
||||||
import { ClaudeAgentSdkEngine } from '../../../infrastructure/engines/claude-agent-sdk/claude-agent-sdk-engine';
|
import { ClaudeAgentSdkEngine } from '../../../infrastructure/engines/claude-agent-sdk/claude-agent-sdk-engine';
|
||||||
|
import { SystemPromptBuilder } from '../../../infrastructure/engines/claude-code-cli/system-prompt-builder';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
@Controller('api/v1/agent')
|
@Controller('api/v1/agent')
|
||||||
|
|
@ -34,11 +35,13 @@ export class AgentController {
|
||||||
private readonly contextService: ConversationContextService,
|
private readonly contextService: ConversationContextService,
|
||||||
private readonly eventPublisher: EventPublisherService,
|
private readonly eventPublisher: EventPublisherService,
|
||||||
private readonly sttService: OpenAISttService,
|
private readonly sttService: OpenAISttService,
|
||||||
|
private readonly systemPromptBuilder: SystemPromptBuilder,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('tasks')
|
@Post('tasks')
|
||||||
async executeTask(
|
async executeTask(
|
||||||
@TenantId() tenantId: string,
|
@TenantId() tenantId: string,
|
||||||
|
@Req() req: any,
|
||||||
@Body() body: {
|
@Body() body: {
|
||||||
prompt: string;
|
prompt: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
|
@ -136,10 +139,19 @@ export class AgentController {
|
||||||
this.logger.log(`[Task ${task.id}] Resuming SDK session: ${resumeSessionId}`);
|
this.logger.log(`[Task ${task.id}] Resuming SDK session: ${resumeSessionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build system prompt with user context so iAgent knows who it's serving
|
||||||
|
const userId: string | undefined = req.user?.sub ?? req.user?.userId;
|
||||||
|
const userEmail: string | undefined = req.user?.email;
|
||||||
|
const systemPrompt = body.systemPrompt || this.systemPromptBuilder.build({
|
||||||
|
tenantId,
|
||||||
|
userId,
|
||||||
|
userEmail,
|
||||||
|
});
|
||||||
|
|
||||||
// Fire-and-forget: run the task stream
|
// Fire-and-forget: run the task stream
|
||||||
this.runTaskStream(engine, session, task, {
|
this.runTaskStream(engine, session, task, {
|
||||||
prompt: body.prompt,
|
prompt: body.prompt,
|
||||||
systemPrompt: body.systemPrompt || '',
|
systemPrompt,
|
||||||
allowedTools: body.allowedTools || [],
|
allowedTools: body.allowedTools || [],
|
||||||
maxTurns: body.maxTurns || 10,
|
maxTurns: body.maxTurns || 10,
|
||||||
conversationHistory: historyForEngine.length > 0 ? historyForEngine : undefined,
|
conversationHistory: historyForEngine.length > 0 ? historyForEngine : undefined,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue