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 { AgentInstanceDeployService } from './infrastructure/services/agent-instance-deploy.service';
|
||||
import { AgentInstanceController } from './interfaces/rest/controllers/agent-instance.controller';
|
||||
import { SystemPromptBuilder } from './infrastructure/engines/claude-code-cli/system-prompt-builder';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -99,6 +100,7 @@ import { AgentInstanceController } from './interfaces/rest/controllers/agent-ins
|
|||
OpenAISttService,
|
||||
AgentInstanceRepository,
|
||||
AgentInstanceDeployService,
|
||||
SystemPromptBuilder,
|
||||
],
|
||||
})
|
||||
export class AgentModule {}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ export class AgentInstance {
|
|||
@Column({ type: 'uuid', name: 'pool_server_id', nullable: true })
|
||||
poolServerId?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, name: 'server_host' })
|
||||
serverHost!: string;
|
||||
@Column({ type: 'varchar', length: 255, name: 'server_host', nullable: true })
|
||||
serverHost?: string;
|
||||
|
||||
@Column({ type: 'int', name: 'ssh_port', default: 22 })
|
||||
sshPort!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, name: 'ssh_user' })
|
||||
sshUser!: string;
|
||||
@Column({ type: 'varchar', length: 100, name: 'ssh_user', nullable: true })
|
||||
sshUser?: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 150, name: 'container_name', unique: true })
|
||||
containerName!: string;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import * as path from 'path';
|
|||
|
||||
export interface SystemPromptContext {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
userEmail?: string;
|
||||
serverContext?: string;
|
||||
skills?: string[];
|
||||
riskBoundary?: string;
|
||||
|
|
@ -33,21 +35,24 @@ export class SystemPromptBuilder {
|
|||
'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' +
|
||||
'## OpenClaw Agent Deployment\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' +
|
||||
' POST /api/v1/agent/instances\n' +
|
||||
' Body: { "name": "<user-given name>", "userId": "<user id>", "usePool": true }\n' +
|
||||
'The instance will be deployed on a pool server automatically. After creation, the user can connect\n' +
|
||||
'their OpenClaw to Telegram, WhatsApp, or other channels via the channel configuration.\n' +
|
||||
'To list existing instances: GET /api/v1/agent/instances\n' +
|
||||
'To stop an instance: PUT /api/v1/agent/instances/:id/stop\n' +
|
||||
'To remove an instance: DELETE /api/v1/agent/instances/:id\n\n' +
|
||||
'When a user says they want to create an OpenClaw, a personal AI agent, or an autonomous agent,\n' +
|
||||
'confirm their desired name, then call the deployment API and report the result.',
|
||||
'You can deploy real OpenClaw (小龙虾) AI agent instances for users. OpenClaw is an open-source autonomous AI agent.\n' +
|
||||
'Use the internal API (no auth required, direct service call):\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' +
|
||||
' List: curl -s http://localhost:3002/api/v1/agent/instances\n' +
|
||||
' Stop: curl -s -X PUT http://localhost:3002/api/v1/agent/instances/<id>/stop\n' +
|
||||
' Delete: curl -s -X DELETE http://localhost:3002/api/v1/agent/instances/<id>\n\n' +
|
||||
'When a user asks to create a OpenClaw / 小龙虾 / personal AI agent / autonomous agent:\n' +
|
||||
'1. Ask for a name if not given\n' +
|
||||
'2. Use the Current User ID from this prompt as userId\n' +
|
||||
'3. Call the create API with Bash and report the result (id, status, containerName)',
|
||||
);
|
||||
|
||||
// Tenant context
|
||||
// Tenant + user context
|
||||
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.)
|
||||
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 { memoryStorage } from 'multer';
|
||||
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 { AgentEnginePort, EngineStreamEvent } from '../../../domain/ports/outbound/agent-engine.port';
|
||||
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';
|
||||
|
||||
@Controller('api/v1/agent')
|
||||
|
|
@ -34,11 +35,13 @@ export class AgentController {
|
|||
private readonly contextService: ConversationContextService,
|
||||
private readonly eventPublisher: EventPublisherService,
|
||||
private readonly sttService: OpenAISttService,
|
||||
private readonly systemPromptBuilder: SystemPromptBuilder,
|
||||
) {}
|
||||
|
||||
@Post('tasks')
|
||||
async executeTask(
|
||||
@TenantId() tenantId: string,
|
||||
@Req() req: any,
|
||||
@Body() body: {
|
||||
prompt: string;
|
||||
sessionId?: string;
|
||||
|
|
@ -136,10 +139,19 @@ export class AgentController {
|
|||
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
|
||||
this.runTaskStream(engine, session, task, {
|
||||
prompt: body.prompt,
|
||||
systemPrompt: body.systemPrompt || '',
|
||||
systemPrompt,
|
||||
allowedTools: body.allowedTools || [],
|
||||
maxTurns: body.maxTurns || 10,
|
||||
conversationHistory: historyForEngine.length > 0 ? historyForEngine : undefined,
|
||||
|
|
|
|||
Loading…
Reference in New Issue