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:
Jason Woltje
2026-02-02 14:33:31 -06:00
parent 24d59e7595
commit c3500783d1
121 changed files with 4123 additions and 58 deletions

View 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");
}
}
}