feat(#66): implement tag filtering in search API endpoint
Add support for filtering search results by tags in the main search endpoint. Changes: - Add tags parameter to SearchQueryDto (comma-separated tag slugs) - Implement tag filtering in SearchService.search() method - Update SQL query to join with knowledge_entry_tags when tags provided - Entries must have ALL specified tags (AND logic) - Add tests for tag filtering (2 controller tests, 2 service tests) - Update endpoint documentation - Fix non-null assertion linting error The search endpoint now supports: - Full-text search with ranking (ts_rank) - Snippet generation with highlighting (ts_headline) - Status filtering - Tag filtering (new) - Pagination Example: GET /api/knowledge/search?q=api&tags=documentation,tutorial All tests pass (25 total), type checking passes, linting passes. Fixes #66 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
120
apps/orchestrator/src/spawner/agent-spawner.service.ts
Normal file
120
apps/orchestrator/src/spawner/agent-spawner.service.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import { randomUUID } from "crypto";
|
||||
import {
|
||||
SpawnAgentRequest,
|
||||
SpawnAgentResponse,
|
||||
AgentSession,
|
||||
AgentType,
|
||||
} from "./types/agent-spawner.types";
|
||||
|
||||
/**
|
||||
* Service responsible for spawning Claude agents using Anthropic SDK
|
||||
*/
|
||||
@Injectable()
|
||||
export class AgentSpawnerService {
|
||||
private readonly logger = new Logger(AgentSpawnerService.name);
|
||||
private readonly anthropic: Anthropic;
|
||||
private readonly sessions = new Map<string, AgentSession>();
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const apiKey = this.configService.get<string>("orchestrator.claude.apiKey");
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error("CLAUDE_API_KEY is not configured");
|
||||
}
|
||||
|
||||
this.anthropic = new Anthropic({
|
||||
apiKey,
|
||||
});
|
||||
|
||||
this.logger.log("AgentSpawnerService initialized with Claude SDK");
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a new agent with the given configuration
|
||||
* @param request Agent spawn request
|
||||
* @returns Agent spawn response with agentId
|
||||
*/
|
||||
spawnAgent(request: SpawnAgentRequest): SpawnAgentResponse {
|
||||
this.logger.log(`Spawning agent for task: ${request.taskId}`);
|
||||
|
||||
// Validate request
|
||||
this.validateSpawnRequest(request);
|
||||
|
||||
// Generate unique agent ID
|
||||
const agentId = randomUUID();
|
||||
const spawnedAt = new Date();
|
||||
|
||||
// Create agent session
|
||||
const session: AgentSession = {
|
||||
agentId,
|
||||
taskId: request.taskId,
|
||||
agentType: request.agentType,
|
||||
state: "spawning",
|
||||
context: request.context,
|
||||
options: request.options,
|
||||
spawnedAt,
|
||||
};
|
||||
|
||||
// Store session
|
||||
this.sessions.set(agentId, session);
|
||||
|
||||
this.logger.log(`Agent spawned successfully: ${agentId} (type: ${request.agentType})`);
|
||||
|
||||
// TODO: Actual Claude SDK integration will be implemented in next iteration
|
||||
// For now, we're just creating the session and tracking it
|
||||
|
||||
return {
|
||||
agentId,
|
||||
state: "spawning",
|
||||
spawnedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get agent session by agentId
|
||||
* @param agentId Unique agent identifier
|
||||
* @returns Agent session or undefined if not found
|
||||
*/
|
||||
getAgentSession(agentId: string): AgentSession | undefined {
|
||||
return this.sessions.get(agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all agent sessions
|
||||
* @returns Array of all agent sessions
|
||||
*/
|
||||
listAgentSessions(): AgentSession[] {
|
||||
return Array.from(this.sessions.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate spawn agent request
|
||||
* @param request Spawn request to validate
|
||||
* @throws Error if validation fails
|
||||
*/
|
||||
private validateSpawnRequest(request: SpawnAgentRequest): void {
|
||||
if (!request.taskId || request.taskId.trim() === "") {
|
||||
throw new Error("taskId is required");
|
||||
}
|
||||
|
||||
const validAgentTypes: AgentType[] = ["worker", "reviewer", "tester"];
|
||||
if (!validAgentTypes.includes(request.agentType)) {
|
||||
throw new Error(`agentType must be one of: ${validAgentTypes.join(", ")}`);
|
||||
}
|
||||
|
||||
if (!request.context.repository || request.context.repository.trim() === "") {
|
||||
throw new Error("context.repository is required");
|
||||
}
|
||||
|
||||
if (!request.context.branch || request.context.branch.trim() === "") {
|
||||
throw new Error("context.branch is required");
|
||||
}
|
||||
|
||||
if (request.context.workItems.length === 0) {
|
||||
throw new Error("context.workItems must not be empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user