Split 4 oversized files (503-679 LOC each) into focused units all under 500 LOC: - tech_file_generator.go → +_prompts, +_prompt_builder, +_fallback - hazard_patterns_extended.go → +_extended2.go (HP074-HP102 extracted) - models.go → +_entities.go, +_api.go (enums / DB entities / API types) - completeness.go → +_gates.go (gate definitions extracted) All files remain in package iace. Zero behavior changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
161 lines
5.1 KiB
Go
161 lines
5.1 KiB
Go
package iace
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
|
|
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ============================================================================
|
|
// TechFileGenerator — LLM-based generation of technical file sections
|
|
// ============================================================================
|
|
|
|
// TechFileGenerator generates technical file section content using LLM and RAG.
|
|
type TechFileGenerator struct {
|
|
llmRegistry *llm.ProviderRegistry
|
|
ragClient *ucca.LegalRAGClient
|
|
store *Store
|
|
}
|
|
|
|
// NewTechFileGenerator creates a new TechFileGenerator.
|
|
func NewTechFileGenerator(registry *llm.ProviderRegistry, ragClient *ucca.LegalRAGClient, store *Store) *TechFileGenerator {
|
|
return &TechFileGenerator{
|
|
llmRegistry: registry,
|
|
ragClient: ragClient,
|
|
store: store,
|
|
}
|
|
}
|
|
|
|
// SectionGenerationContext holds all project data needed for LLM section generation.
|
|
type SectionGenerationContext struct {
|
|
Project *Project
|
|
Components []Component
|
|
Hazards []Hazard
|
|
Assessments map[uuid.UUID][]RiskAssessment // keyed by hazardID
|
|
Mitigations map[uuid.UUID][]Mitigation // keyed by hazardID
|
|
Classifications []RegulatoryClassification
|
|
Evidence []Evidence
|
|
RAGContext string // aggregated text from RAG search
|
|
}
|
|
|
|
// ============================================================================
|
|
// BuildSectionContext — loads all project data + RAG context
|
|
// ============================================================================
|
|
|
|
// BuildSectionContext loads project data and RAG context for a given section type.
|
|
func (g *TechFileGenerator) BuildSectionContext(ctx context.Context, projectID uuid.UUID, sectionType string) (*SectionGenerationContext, error) {
|
|
// Load project
|
|
project, err := g.store.GetProject(ctx, projectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load project: %w", err)
|
|
}
|
|
if project == nil {
|
|
return nil, fmt.Errorf("project %s not found", projectID)
|
|
}
|
|
|
|
// Load components
|
|
components, err := g.store.ListComponents(ctx, projectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load components: %w", err)
|
|
}
|
|
|
|
// Load hazards
|
|
hazards, err := g.store.ListHazards(ctx, projectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load hazards: %w", err)
|
|
}
|
|
|
|
// Load assessments and mitigations per hazard
|
|
assessments := make(map[uuid.UUID][]RiskAssessment)
|
|
mitigations := make(map[uuid.UUID][]Mitigation)
|
|
for _, h := range hazards {
|
|
a, err := g.store.ListAssessments(ctx, h.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load assessments for hazard %s: %w", h.ID, err)
|
|
}
|
|
assessments[h.ID] = a
|
|
|
|
m, err := g.store.ListMitigations(ctx, h.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load mitigations for hazard %s: %w", h.ID, err)
|
|
}
|
|
mitigations[h.ID] = m
|
|
}
|
|
|
|
// Load classifications
|
|
classifications, err := g.store.GetClassifications(ctx, projectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load classifications: %w", err)
|
|
}
|
|
|
|
// Load evidence
|
|
evidence, err := g.store.ListEvidence(ctx, projectID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load evidence: %w", err)
|
|
}
|
|
|
|
// Perform RAG search for section-specific context
|
|
ragContext := ""
|
|
if g.ragClient != nil {
|
|
ragQuery := buildRAGQuery(sectionType)
|
|
results, ragErr := g.ragClient.SearchCollection(ctx, "bp_iace_libraries", ragQuery, nil, 5)
|
|
if ragErr == nil && len(results) > 0 {
|
|
var ragParts []string
|
|
for _, r := range results {
|
|
entry := fmt.Sprintf("[%s] %s", r.RegulationShort, truncateForPrompt(r.Text, 400))
|
|
ragParts = append(ragParts, entry)
|
|
}
|
|
ragContext = strings.Join(ragParts, "\n\n")
|
|
}
|
|
// RAG failure is non-fatal — we proceed without context
|
|
}
|
|
|
|
return &SectionGenerationContext{
|
|
Project: project,
|
|
Components: components,
|
|
Hazards: hazards,
|
|
Assessments: assessments,
|
|
Mitigations: mitigations,
|
|
Classifications: classifications,
|
|
Evidence: evidence,
|
|
RAGContext: ragContext,
|
|
}, nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// GenerateSection — main entry point
|
|
// ============================================================================
|
|
|
|
// GenerateSection generates the content for a technical file section using LLM.
|
|
// If LLM is unavailable, returns an enhanced placeholder with project data.
|
|
func (g *TechFileGenerator) GenerateSection(ctx context.Context, projectID uuid.UUID, sectionType string) (string, error) {
|
|
sctx, err := g.BuildSectionContext(ctx, projectID, sectionType)
|
|
if err != nil {
|
|
return "", fmt.Errorf("build section context: %w", err)
|
|
}
|
|
|
|
// Build prompts
|
|
systemPrompt := getSystemPrompt(sectionType)
|
|
userPrompt := buildUserPrompt(sctx, sectionType)
|
|
|
|
// Attempt LLM generation
|
|
resp, err := g.llmRegistry.Chat(ctx, &llm.ChatRequest{
|
|
Messages: []llm.Message{
|
|
{Role: "system", Content: systemPrompt},
|
|
{Role: "user", Content: userPrompt},
|
|
},
|
|
Temperature: 0.15,
|
|
MaxTokens: 4096,
|
|
})
|
|
if err != nil {
|
|
// LLM unavailable — return structured fallback with real project data
|
|
return buildFallbackContent(sctx, sectionType), nil
|
|
}
|
|
|
|
return resp.Message.Content, nil
|
|
}
|