package service import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/logging" "github.com/orchard9/rdev/internal/port" ) // ArchitectService orchestrates conversational project design with Claude. type ArchitectService struct { conversationService *ConversationService blueprintService *BlueprintService agentRegistry port.CodeAgentRegistry projectRepo port.ProjectRepository } // NewArchitectService creates a new architect service. func NewArchitectService( conversationService *ConversationService, blueprintService *BlueprintService, agentRegistry port.CodeAgentRegistry, projectRepo port.ProjectRepository, ) *ArchitectService { return &ArchitectService{ conversationService: conversationService, blueprintService: blueprintService, agentRegistry: agentRegistry, projectRepo: projectRepo, } } // StartConversation begins a new architectural conversation. func (s *ArchitectService) StartConversation(ctx context.Context, projectID, initialPrompt string) (*domain.ConversationWithMessages, error) { // Create conversation conv, err := s.conversationService.CreateConversation(ctx, projectID, "Architectural Design") if err != nil { return nil, fmt.Errorf("create conversation: %w", err) } // Add user's initial message _, err = s.conversationService.AddMessage(ctx, conv.ID, domain.MessageRoleUser, initialPrompt) if err != nil { return nil, fmt.Errorf("add user message: %w", err) } // Get agent response response, err := s.askArchitect(ctx, projectID, conv.ID, initialPrompt) if err != nil { return nil, fmt.Errorf("ask architect: %w", err) } // Add assistant's response _, err = s.conversationService.AddMessage(ctx, conv.ID, domain.MessageRoleAssistant, response) if err != nil { return nil, fmt.Errorf("add assistant message: %w", err) } log := logging.FromContext(ctx) log.Info("architectural conversation started", "conversation_id", conv.ID, logging.FieldProjectID, projectID, logging.FieldOperation, "start_architect_conversation", ) return s.conversationService.GetConversationWithMessages(ctx, conv.ID) } // ContinueConversation adds a message and gets agent response. func (s *ArchitectService) ContinueConversation(ctx context.Context, conversationID domain.ConversationID, userMessage string) (*domain.Message, error) { // Get conversation to find project conv, err := s.conversationService.GetConversation(ctx, conversationID) if err != nil { return nil, err } // Add user message _, err = s.conversationService.AddMessage(ctx, conversationID, domain.MessageRoleUser, userMessage) if err != nil { return nil, fmt.Errorf("add user message: %w", err) } // Get conversation history for context messages, err := s.conversationService.GetMessages(ctx, conversationID) if err != nil { return nil, err } // Build context from history context := s.buildConversationContext(messages) // Get agent response response, err := s.askArchitect(ctx, conv.ProjectID, conversationID, context) if err != nil { return nil, fmt.Errorf("ask architect: %w", err) } // Add assistant's response return s.conversationService.AddMessage(ctx, conversationID, domain.MessageRoleAssistant, response) } // GenerateBlueprint creates a blueprint from a conversation. func (s *ArchitectService) GenerateBlueprint(ctx context.Context, conversationID domain.ConversationID, blueprintName string) (*domain.Blueprint, error) { conv, err := s.conversationService.GetConversation(ctx, conversationID) if err != nil { return nil, err } messages, err := s.conversationService.GetMessages(ctx, conversationID) if err != nil { return nil, err } // Extract structured spec from conversation spec, err := s.extractSpecFromMessages(ctx, conv.ProjectID, messages) if err != nil { return nil, fmt.Errorf("extract spec: %w", err) } // Create blueprint return s.blueprintService.CreateBlueprint(ctx, conv.ProjectID, blueprintName, "Generated from architectural conversation", spec) } // askArchitect sends a prompt to Claude and gets the response. func (s *ArchitectService) askArchitect(ctx context.Context, projectID string, conversationID domain.ConversationID, prompt string) (string, error) { agent := s.agentRegistry.Default() if agent == nil { return "", fmt.Errorf("no agent available") } project, err := s.projectRepo.Get(ctx, domain.ProjectID(projectID)) if err != nil { return "", fmt.Errorf("resolve project: %w", err) } // Prepare architect-specific system prompt systemPrompt := `You are an expert software architect helping design a new project. Your role is to: 1. Ask clarifying questions to understand requirements deeply 2. Identify technical architecture and stack choices 3. Recommend component structure (monorepo, microservices, etc.) 4. Define infrastructure needs (database, cache, storage, messaging) 5. Consider scalability, maintainability, and team capabilities Guidelines: - Ask one focused question at a time - Provide specific recommendations with trade-offs - Think about the full system: data models, APIs, UI components, infrastructure - When you have enough information, summarize the architecture clearly Current conversation context:` fullPrompt := systemPrompt + "\n\n" + prompt agentReq := &domain.AgentRequest{ Prompt: fullPrompt, ProjectID: project.ID, Timeout: 2 * time.Minute, Metadata: map[string]string{ "conversation_id": string(conversationID), "purpose": "architect", }, } var output strings.Builder _, err = agent.Execute(ctx, agentReq, func(event domain.AgentEvent) { if event.Type == domain.AgentEventOutput { output.WriteString(event.Content) } }) if err != nil { return "", fmt.Errorf("agent execution: %w", err) } return output.String(), nil } // buildConversationContext combines message history into a single context string. func (s *ArchitectService) buildConversationContext(messages []*domain.Message) string { var context strings.Builder for _, msg := range messages { role := string(msg.Role) context.WriteString(fmt.Sprintf("%s: %s\n\n", role, msg.Content)) } return context.String() } // extractSpecFromMessages parses conversation to extract structured blueprint spec. func (s *ArchitectService) extractSpecFromMessages(ctx context.Context, projectID string, messages []*domain.Message) (map[string]any, error) { // Use Claude to analyze conversation and extract structured data agent := s.agentRegistry.Default() if agent == nil { return nil, fmt.Errorf("no agent available") } project, err := s.projectRepo.Get(ctx, domain.ProjectID(projectID)) if err != nil { return nil, fmt.Errorf("resolve project: %w", err) } // Build conversation transcript transcript := s.buildConversationContext(messages) // Prompt to extract structured spec extractionPrompt := fmt.Sprintf(`Analyze this architectural conversation and extract a structured JSON specification. Conversation transcript: %s Extract and return ONLY a valid JSON object with this structure: { "version": "1.0", "architecture": { "type": "monorepo|microservices|monolith", "description": "...", "components": [ {"name": "...", "type": "service|app|worker|cli", "description": "..."} ] }, "data_models": [ {"name": "...", "description": "...", "fields": [...]} ], "api_endpoints": [ {"path": "...", "method": "GET|POST|PUT|DELETE", "description": "..."} ], "infrastructure": { "database": {"type": "postgres|mysql|mongo", "required": true|false}, "cache": {"type": "redis|memcached", "required": true|false}, "storage": {"type": "s3|gcs", "required": true|false}, "messaging": {"type": "rabbitmq|kafka", "required": true|false} }, "features": [ {"name": "...", "description": "...", "priority": "high|medium|low"} ] } Return ONLY the JSON, no other text.`, transcript) agentReq := &domain.AgentRequest{ Prompt: extractionPrompt, ProjectID: project.ID, Timeout: 2 * time.Minute, Metadata: map[string]string{ "purpose": "spec-extraction", }, } var output strings.Builder _, err = agent.Execute(ctx, agentReq, func(event domain.AgentEvent) { if event.Type == domain.AgentEventOutput { output.WriteString(event.Content) } }) if err != nil { return nil, fmt.Errorf("agent execution: %w", err) } // Parse JSON response response := output.String() // Extract JSON from markdown code blocks if present if strings.Contains(response, "```json") { start := strings.Index(response, "```json") + 7 end := strings.Index(response[start:], "```") if end != -1 { response = response[start : start+end] } } else if strings.Contains(response, "```") { start := strings.Index(response, "```") + 3 end := strings.Index(response[start:], "```") if end != -1 { response = response[start : start+end] } } response = strings.TrimSpace(response) var spec map[string]any if err := json.Unmarshal([]byte(response), &spec); err != nil { // Fallback: create basic spec if parsing fails log := logging.FromContext(ctx) log.Warn("failed to parse agent spec extraction, using fallback", logging.FieldError, err, logging.FieldOperation, "extract_blueprint_spec", "response_length", len(response), "message_count", len(messages), ) spec = map[string]any{ "version": "1.0", "generated_at": time.Now().Format(time.RFC3339), "message_count": len(messages), "extraction_failed": true, } } return spec, nil }