package services import ( "context" "encoding/base64" "encoding/json" "fmt" "strings" "time" "github.com/anthropics/anthropic-sdk-go" "github.com/anthropics/anthropic-sdk-go/option" "github.com/google/uuid" "github.com/jmoiron/sqlx" "mgit.msbls.de/m/KanzlAI-mGMT/internal/models" ) type AIService struct { client anthropic.Client db *sqlx.DB youpcDB *sqlx.DB // read-only connection to youpc.org for similar case finder (may be nil) } func NewAIService(apiKey string, db *sqlx.DB, youpcDB *sqlx.DB) *AIService { client := anthropic.NewClient(option.WithAPIKey(apiKey)) return &AIService{client: client, db: db, youpcDB: youpcDB} } // ExtractedDeadline represents a deadline extracted by AI from a document. type ExtractedDeadline struct { Title string `json:"title"` DueDate *string `json:"due_date"` DurationValue int `json:"duration_value"` DurationUnit string `json:"duration_unit"` Timing string `json:"timing"` TriggerEvent string `json:"trigger_event"` RuleReference string `json:"rule_reference"` Confidence float64 `json:"confidence"` SourceQuote string `json:"source_quote"` } type extractDeadlinesToolInput struct { Deadlines []ExtractedDeadline `json:"deadlines"` } var deadlineExtractionTool = anthropic.ToolParam{ Name: "extract_deadlines", Description: anthropic.String("Extract all legal deadlines found in the document. Return each deadline with its details."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "deadlines": map[string]any{ "type": "array", "description": "List of extracted deadlines", "items": map[string]any{ "type": "object", "properties": map[string]any{ "title": map[string]any{ "type": "string", "description": "Short title describing the deadline (e.g. 'Statement of Defence', 'Reply to Counterclaim')", }, "due_date": map[string]any{ "type": []string{"string", "null"}, "description": "Absolute due date in YYYY-MM-DD format if determinable, null otherwise", }, "duration_value": map[string]any{ "type": "integer", "description": "Numeric duration value (e.g. 3 for '3 months')", }, "duration_unit": map[string]any{ "type": "string", "enum": []string{"days", "weeks", "months"}, "description": "Unit of the duration period", }, "timing": map[string]any{ "type": "string", "enum": []string{"after", "before"}, "description": "Whether the deadline is before or after the trigger event", }, "trigger_event": map[string]any{ "type": "string", "description": "The event that triggers this deadline (e.g. 'service of the Statement of Claim')", }, "rule_reference": map[string]any{ "type": "string", "description": "Legal rule reference (e.g. 'Rule 23 RoP', 'Rule 222 RoP', '§ 276 ZPO')", }, "confidence": map[string]any{ "type": "number", "minimum": 0, "maximum": 1, "description": "Confidence score from 0.0 to 1.0", }, "source_quote": map[string]any{ "type": "string", "description": "The exact quote from the document where this deadline was found", }, }, "required": []string{"title", "duration_value", "duration_unit", "timing", "trigger_event", "rule_reference", "confidence", "source_quote"}, }, }, }, Required: []string{"deadlines"}, }, } const extractionSystemPrompt = `You are a legal deadline extraction assistant for German and UPC (Unified Patent Court) patent litigation. Your task is to extract all legal deadlines, time limits, and procedural time periods from the provided document. For each deadline found, extract: - A clear title describing the deadline - The absolute due date if it can be determined from the document - The duration (value + unit: days/weeks/months) - Whether it runs before or after a trigger event - The trigger event that starts the deadline - The legal rule reference (e.g. Rule 23 RoP, § 276 ZPO) - Your confidence level (0.0-1.0) in the extraction - The exact source quote from the document Be thorough: extract every deadline mentioned, including conditional ones. If a deadline references another deadline (e.g. "within 2 months of the defence"), capture that relationship in the trigger_event field. If the document contains no deadlines, return an empty list.` // ExtractDeadlines sends a document (PDF or text) to Claude for deadline extraction. func (s *AIService) ExtractDeadlines(ctx context.Context, pdfData []byte, text string) ([]ExtractedDeadline, error) { var contentBlocks []anthropic.ContentBlockParamUnion if len(pdfData) > 0 { encoded := base64.StdEncoding.EncodeToString(pdfData) contentBlocks = append(contentBlocks, anthropic.ContentBlockParamUnion{ OfDocument: &anthropic.DocumentBlockParam{ Source: anthropic.DocumentBlockParamSourceUnion{ OfBase64: &anthropic.Base64PDFSourceParam{ Data: encoded, }, }, }, }) contentBlocks = append(contentBlocks, anthropic.NewTextBlock("Extract all legal deadlines from this document.")) } else if text != "" { contentBlocks = append(contentBlocks, anthropic.NewTextBlock("Extract all legal deadlines from the following text:\n\n"+text)) } else { return nil, fmt.Errorf("either pdf_data or text must be provided") } msg, err := s.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet4_5, MaxTokens: 4096, System: []anthropic.TextBlockParam{ {Text: extractionSystemPrompt}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(contentBlocks...), }, Tools: []anthropic.ToolUnionParam{ {OfTool: &deadlineExtractionTool}, }, ToolChoice: anthropic.ToolChoiceParamOfTool("extract_deadlines"), }) if err != nil { return nil, fmt.Errorf("claude API call: %w", err) } // Find the tool_use block in the response for _, block := range msg.Content { if block.Type == "tool_use" && block.Name == "extract_deadlines" { var input extractDeadlinesToolInput if err := json.Unmarshal(block.Input, &input); err != nil { return nil, fmt.Errorf("parsing tool output: %w", err) } return input.Deadlines, nil } } return nil, fmt.Errorf("no tool_use block in response") } const summarizeSystemPrompt = `You are a legal case summary assistant for German and UPC patent litigation case management. Given a case's details, recent events, and deadlines, produce a concise 2-3 sentence summary of what matters right now. Focus on: - The most urgent upcoming deadline - Recent significant events - The current procedural stage Write in clear, professional language suitable for a lawyer reviewing their case list. Be specific about dates and deadlines.` // SummarizeCase generates an AI summary for a case and caches it in the database. func (s *AIService) SummarizeCase(ctx context.Context, tenantID, caseID uuid.UUID) (string, error) { // Load case var c models.Case err := s.db.GetContext(ctx, &c, "SELECT * FROM cases WHERE id = $1 AND tenant_id = $2", caseID, tenantID) if err != nil { return "", fmt.Errorf("loading case: %w", err) } // Load recent events var events []models.CaseEvent if err := s.db.SelectContext(ctx, &events, "SELECT * FROM case_events WHERE case_id = $1 AND tenant_id = $2 ORDER BY created_at DESC LIMIT 10", caseID, tenantID); err != nil { return "", fmt.Errorf("loading events: %w", err) } // Load active deadlines var deadlines []models.Deadline if err := s.db.SelectContext(ctx, &deadlines, "SELECT * FROM deadlines WHERE case_id = $1 AND tenant_id = $2 AND status = 'active' ORDER BY due_date ASC LIMIT 10", caseID, tenantID); err != nil { return "", fmt.Errorf("loading deadlines: %w", err) } // Build context text caseInfo := fmt.Sprintf("Case: %s — %s\nStatus: %s", c.CaseNumber, c.Title, c.Status) if c.Court != nil { caseInfo += fmt.Sprintf("\nCourt: %s", *c.Court) } if c.CourtRef != nil { caseInfo += fmt.Sprintf("\nCourt Reference: %s", *c.CourtRef) } if c.CaseType != nil { caseInfo += fmt.Sprintf("\nType: %s", *c.CaseType) } eventText := "\n\nRecent Events:" if len(events) == 0 { eventText += "\nNo events recorded." } for _, e := range events { eventText += fmt.Sprintf("\n- [%s] %s", e.CreatedAt.Format("2006-01-02"), e.Title) if e.Description != nil { eventText += fmt.Sprintf(": %s", *e.Description) } } deadlineText := "\n\nUpcoming Deadlines:" if len(deadlines) == 0 { deadlineText += "\nNo active deadlines." } for _, d := range deadlines { deadlineText += fmt.Sprintf("\n- %s: due %s (status: %s)", d.Title, d.DueDate, d.Status) if d.Description != nil { deadlineText += fmt.Sprintf(" — %s", *d.Description) } } prompt := caseInfo + eventText + deadlineText msg, err := s.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet4_5, MaxTokens: 512, System: []anthropic.TextBlockParam{ {Text: summarizeSystemPrompt}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Summarize the current state of this case:\n\n" + prompt)), }, }) if err != nil { return "", fmt.Errorf("claude API call: %w", err) } // Extract text from response var summary string for _, block := range msg.Content { if block.Type == "text" { summary += block.Text } } if summary == "" { return "", fmt.Errorf("empty response from Claude") } // Cache summary in database _, err = s.db.ExecContext(ctx, "UPDATE cases SET ai_summary = $1, updated_at = $2 WHERE id = $3 AND tenant_id = $4", summary, time.Now(), caseID, tenantID) if err != nil { return "", fmt.Errorf("caching summary: %w", err) } return summary, nil } // --- Document Drafting --- // DocumentDraft represents an AI-generated document draft. type DocumentDraft struct { Title string `json:"title"` Content string `json:"content"` Language string `json:"language"` } // templateDescriptions maps template type IDs to descriptions for Claude. var templateDescriptions = map[string]string{ "klageschrift": "Klageschrift (Statement of Claim) — formal complaint initiating legal proceedings", "klageerwiderung": "Klageerwiderung (Statement of Defence) — formal response to a statement of claim", "abmahnung": "Abmahnung (Cease and Desist Letter) — formal warning letter demanding cessation of an activity", "schriftsatz": "Schriftsatz (Legal Brief) — formal legal submission to the court", "berufung": "Berufungsschrift (Appeal Brief) — formal appeal against a court decision", "antrag": "Antrag (Motion/Application) — formal application or motion to the court", "stellungnahme": "Stellungnahme (Statement/Position Paper) — formal response or position paper", "gutachten": "Gutachten (Legal Opinion/Expert Report) — detailed legal analysis or opinion", "vertrag": "Vertrag (Contract/Agreement) — legal contract or agreement between parties", "vollmacht": "Vollmacht (Power of Attorney) — formal authorization document", "upc_claim": "UPC Statement of Claim — claim filed at the Unified Patent Court", "upc_defence": "UPC Statement of Defence — defence filed at the Unified Patent Court", "upc_counterclaim": "UPC Counterclaim for Revocation — counterclaim for patent revocation at the UPC", "upc_injunction": "UPC Application for Provisional Measures — application for injunctive relief at the UPC", } const draftDocumentSystemPrompt = `You are an expert legal document drafter for German and UPC (Unified Patent Court) patent litigation. You draft professional legal documents in the requested language, following proper legal formatting conventions. Guidelines: - Use proper legal structure with numbered sections and paragraphs - Include standard legal formalities (headers, salutations, signatures block) - Reference relevant legal provisions (BGB, ZPO, UPC Rules of Procedure, etc.) - Use precise legal terminology appropriate for the jurisdiction - Include placeholders in [BRACKETS] for information that needs to be filled in - Base the content on the provided case data and instructions - Output the document as clean text with proper formatting` // DraftDocument generates an AI-drafted legal document based on case data and a template type. func (s *AIService) DraftDocument(ctx context.Context, tenantID, caseID uuid.UUID, templateType, instructions, language string) (*DocumentDraft, error) { if language == "" { language = "de" } langLabel := "German" if language == "en" { langLabel = "English" } else if language == "fr" { langLabel = "French" } // Load case data var c models.Case if err := s.db.GetContext(ctx, &c, "SELECT * FROM cases WHERE id = $1 AND tenant_id = $2", caseID, tenantID); err != nil { return nil, fmt.Errorf("loading case: %w", err) } // Load parties var parties []models.Party _ = s.db.SelectContext(ctx, &parties, "SELECT * FROM parties WHERE case_id = $1 AND tenant_id = $2", caseID, tenantID) // Load recent events var events []models.CaseEvent _ = s.db.SelectContext(ctx, &events, "SELECT * FROM case_events WHERE case_id = $1 AND tenant_id = $2 ORDER BY created_at DESC LIMIT 15", caseID, tenantID) // Load active deadlines var deadlines []models.Deadline _ = s.db.SelectContext(ctx, &deadlines, "SELECT * FROM deadlines WHERE case_id = $1 AND tenant_id = $2 AND status = 'active' ORDER BY due_date ASC LIMIT 10", caseID, tenantID) // Load documents metadata for context var documents []models.Document _ = s.db.SelectContext(ctx, &documents, "SELECT id, title, doc_type, created_at FROM documents WHERE case_id = $1 AND tenant_id = $2 ORDER BY created_at DESC LIMIT 10", caseID, tenantID) // Build context var b strings.Builder b.WriteString(fmt.Sprintf("Case: %s — %s\nStatus: %s\n", c.CaseNumber, c.Title, c.Status)) if c.Court != nil { b.WriteString(fmt.Sprintf("Court: %s\n", *c.Court)) } if c.CourtRef != nil { b.WriteString(fmt.Sprintf("Court Reference: %s\n", *c.CourtRef)) } if c.CaseType != nil { b.WriteString(fmt.Sprintf("Type: %s\n", *c.CaseType)) } if len(parties) > 0 { b.WriteString("\nParties:\n") for _, p := range parties { role := "unknown role" if p.Role != nil { role = *p.Role } b.WriteString(fmt.Sprintf("- %s (%s)", p.Name, role)) if p.Representative != nil { b.WriteString(fmt.Sprintf(" — represented by %s", *p.Representative)) } b.WriteString("\n") } } if len(events) > 0 { b.WriteString("\nRecent Events:\n") for _, e := range events { b.WriteString(fmt.Sprintf("- [%s] %s", e.CreatedAt.Format("2006-01-02"), e.Title)) if e.Description != nil { b.WriteString(fmt.Sprintf(": %s", *e.Description)) } b.WriteString("\n") } } if len(deadlines) > 0 { b.WriteString("\nUpcoming Deadlines:\n") for _, d := range deadlines { b.WriteString(fmt.Sprintf("- %s: due %s\n", d.Title, d.DueDate)) } } templateDesc, ok := templateDescriptions[templateType] if !ok { templateDesc = templateType } prompt := fmt.Sprintf(`Draft a %s for this case in %s. Document type: %s Case context: %s Additional instructions from the lawyer: %s Generate the complete document now.`, templateDesc, langLabel, templateDesc, b.String(), instructions) msg, err := s.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet4_20250514, MaxTokens: 8192, System: []anthropic.TextBlockParam{ {Text: draftDocumentSystemPrompt}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)), }, }) if err != nil { return nil, fmt.Errorf("claude API call: %w", err) } var content string for _, block := range msg.Content { if block.Type == "text" { content += block.Text } } if content == "" { return nil, fmt.Errorf("empty response from Claude") } title := fmt.Sprintf("%s — %s", templateDesc, c.CaseNumber) return &DocumentDraft{ Title: title, Content: content, Language: language, }, nil } // --- Case Strategy --- // StrategyRecommendation represents an AI-generated case strategy analysis. type StrategyRecommendation struct { Summary string `json:"summary"` NextSteps []StrategyStep `json:"next_steps"` RiskAssessment []RiskItem `json:"risk_assessment"` Timeline []TimelineItem `json:"timeline"` } type StrategyStep struct { Priority string `json:"priority"` // high, medium, low Action string `json:"action"` Reasoning string `json:"reasoning"` Deadline string `json:"deadline,omitempty"` } type RiskItem struct { Level string `json:"level"` // high, medium, low Risk string `json:"risk"` Mitigation string `json:"mitigation"` } type TimelineItem struct { Date string `json:"date"` Event string `json:"event"` Importance string `json:"importance"` // critical, important, routine } type strategyToolInput struct { Summary string `json:"summary"` NextSteps []StrategyStep `json:"next_steps"` RiskAssessment []RiskItem `json:"risk_assessment"` Timeline []TimelineItem `json:"timeline"` } var caseStrategyTool = anthropic.ToolParam{ Name: "case_strategy", Description: anthropic.String("Provide strategic case analysis with next steps, risk assessment, and timeline optimization."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "summary": map[string]any{ "type": "string", "description": "Executive summary of the case situation and strategic outlook (2-4 sentences)", }, "next_steps": map[string]any{ "type": "array", "description": "Recommended next actions in priority order", "items": map[string]any{ "type": "object", "properties": map[string]any{ "priority": map[string]any{ "type": "string", "enum": []string{"high", "medium", "low"}, }, "action": map[string]any{ "type": "string", "description": "Specific recommended action", }, "reasoning": map[string]any{ "type": "string", "description": "Why this action is recommended", }, "deadline": map[string]any{ "type": "string", "description": "Suggested deadline in YYYY-MM-DD format, if applicable", }, }, "required": []string{"priority", "action", "reasoning"}, }, }, "risk_assessment": map[string]any{ "type": "array", "description": "Key risks and mitigation strategies", "items": map[string]any{ "type": "object", "properties": map[string]any{ "level": map[string]any{ "type": "string", "enum": []string{"high", "medium", "low"}, }, "risk": map[string]any{ "type": "string", "description": "Description of the risk", }, "mitigation": map[string]any{ "type": "string", "description": "Recommended mitigation strategy", }, }, "required": []string{"level", "risk", "mitigation"}, }, }, "timeline": map[string]any{ "type": "array", "description": "Optimized timeline of upcoming milestones and events", "items": map[string]any{ "type": "object", "properties": map[string]any{ "date": map[string]any{ "type": "string", "description": "Date in YYYY-MM-DD format", }, "event": map[string]any{ "type": "string", "description": "Description of the milestone or event", }, "importance": map[string]any{ "type": "string", "enum": []string{"critical", "important", "routine"}, }, }, "required": []string{"date", "event", "importance"}, }, }, }, Required: []string{"summary", "next_steps", "risk_assessment", "timeline"}, }, } const caseStrategySystemPrompt = `You are a senior litigation strategist specializing in German law and UPC (Unified Patent Court) patent proceedings. Analyze the case thoroughly and provide: 1. An executive summary of the current strategic position 2. Prioritized next steps with clear reasoning 3. Risk assessment with mitigation strategies 4. An optimized timeline of upcoming milestones Consider: - Procedural deadlines and their implications - Strength of the parties' positions based on available information - Potential settlement opportunities - Cost-efficiency of different strategic approaches - UPC-specific procedural peculiarities if applicable (bifurcation, preliminary injunctions, etc.) Be practical and actionable. Avoid generic advice — tailor recommendations to the specific case data provided.` // CaseStrategy analyzes a case and returns strategic recommendations. func (s *AIService) CaseStrategy(ctx context.Context, tenantID, caseID uuid.UUID) (*StrategyRecommendation, error) { // Load case var c models.Case if err := s.db.GetContext(ctx, &c, "SELECT * FROM cases WHERE id = $1 AND tenant_id = $2", caseID, tenantID); err != nil { return nil, fmt.Errorf("loading case: %w", err) } // Load parties var parties []models.Party _ = s.db.SelectContext(ctx, &parties, "SELECT * FROM parties WHERE case_id = $1 AND tenant_id = $2", caseID, tenantID) // Load all events var events []models.CaseEvent _ = s.db.SelectContext(ctx, &events, "SELECT * FROM case_events WHERE case_id = $1 AND tenant_id = $2 ORDER BY created_at DESC LIMIT 25", caseID, tenantID) // Load all deadlines (active + completed for context) var deadlines []models.Deadline _ = s.db.SelectContext(ctx, &deadlines, "SELECT * FROM deadlines WHERE case_id = $1 AND tenant_id = $2 ORDER BY due_date ASC LIMIT 20", caseID, tenantID) // Load documents metadata var documents []models.Document _ = s.db.SelectContext(ctx, &documents, "SELECT id, title, doc_type, created_at FROM documents WHERE case_id = $1 AND tenant_id = $2 ORDER BY created_at DESC LIMIT 15", caseID, tenantID) // Build comprehensive context var b strings.Builder b.WriteString(fmt.Sprintf("Case: %s — %s\nStatus: %s\n", c.CaseNumber, c.Title, c.Status)) if c.Court != nil { b.WriteString(fmt.Sprintf("Court: %s\n", *c.Court)) } if c.CourtRef != nil { b.WriteString(fmt.Sprintf("Court Reference: %s\n", *c.CourtRef)) } if c.CaseType != nil { b.WriteString(fmt.Sprintf("Type: %s\n", *c.CaseType)) } if len(parties) > 0 { b.WriteString("\nParties:\n") for _, p := range parties { role := "unknown" if p.Role != nil { role = *p.Role } b.WriteString(fmt.Sprintf("- %s (%s)", p.Name, role)) if p.Representative != nil { b.WriteString(fmt.Sprintf(" — represented by %s", *p.Representative)) } b.WriteString("\n") } } if len(events) > 0 { b.WriteString("\nCase Events (chronological):\n") for _, e := range events { b.WriteString(fmt.Sprintf("- [%s] %s", e.CreatedAt.Format("2006-01-02"), e.Title)) if e.Description != nil { b.WriteString(fmt.Sprintf(": %s", *e.Description)) } b.WriteString("\n") } } if len(deadlines) > 0 { b.WriteString("\nDeadlines:\n") for _, d := range deadlines { b.WriteString(fmt.Sprintf("- %s: due %s (status: %s)\n", d.Title, d.DueDate, d.Status)) } } if len(documents) > 0 { b.WriteString("\nDocuments on file:\n") for _, d := range documents { docType := "" if d.DocType != nil { docType = fmt.Sprintf(" [%s]", *d.DocType) } b.WriteString(fmt.Sprintf("- %s%s (%s)\n", d.Title, docType, d.CreatedAt.Format("2006-01-02"))) } } msg, err := s.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeOpus4_6, MaxTokens: 4096, System: []anthropic.TextBlockParam{ {Text: caseStrategySystemPrompt}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock("Analyze this case and provide strategic recommendations:\n\n" + b.String())), }, Tools: []anthropic.ToolUnionParam{ {OfTool: &caseStrategyTool}, }, ToolChoice: anthropic.ToolChoiceParamOfTool("case_strategy"), }) if err != nil { return nil, fmt.Errorf("claude API call: %w", err) } for _, block := range msg.Content { if block.Type == "tool_use" && block.Name == "case_strategy" { var input strategyToolInput if err := json.Unmarshal(block.Input, &input); err != nil { return nil, fmt.Errorf("parsing strategy output: %w", err) } result := &StrategyRecommendation{ Summary: input.Summary, NextSteps: input.NextSteps, RiskAssessment: input.RiskAssessment, Timeline: input.Timeline, } // Cache in database strategyJSON, _ := json.Marshal(result) _, _ = s.db.ExecContext(ctx, "UPDATE cases SET ai_summary = $1, updated_at = $2 WHERE id = $3 AND tenant_id = $4", string(strategyJSON), time.Now(), caseID, tenantID) return result, nil } } return nil, fmt.Errorf("no tool_use block in response") } // --- Similar Case Finder --- // SimilarCase represents a UPC case found to be similar. type SimilarCase struct { CaseNumber string `json:"case_number"` Title string `json:"title"` Court string `json:"court"` Date string `json:"date"` Relevance float64 `json:"relevance"` // 0.0-1.0 Explanation string `json:"explanation"` // why this case is similar KeyHoldings string `json:"key_holdings"` // relevant holdings URL string `json:"url,omitempty"` // link to youpc.org } // youpcCase represents a case from the youpc.org database. type youpcCase struct { ID string `db:"id" json:"id"` CaseNumber *string `db:"case_number" json:"case_number"` Title *string `db:"title" json:"title"` Court *string `db:"court" json:"court"` DecisionDate *string `db:"decision_date" json:"decision_date"` CaseType *string `db:"case_type" json:"case_type"` Outcome *string `db:"outcome" json:"outcome"` PatentNumbers *string `db:"patent_numbers" json:"patent_numbers"` Summary *string `db:"summary" json:"summary"` Claimant *string `db:"claimant" json:"claimant"` Defendant *string `db:"defendant" json:"defendant"` } type similarCaseToolInput struct { Cases []struct { CaseID string `json:"case_id"` Relevance float64 `json:"relevance"` Explanation string `json:"explanation"` KeyHoldings string `json:"key_holdings"` } `json:"cases"` } var similarCaseTool = anthropic.ToolParam{ Name: "rank_similar_cases", Description: anthropic.String("Rank the provided UPC cases by relevance to the query case and explain why each is similar."), InputSchema: anthropic.ToolInputSchemaParam{ Properties: map[string]any{ "cases": map[string]any{ "type": "array", "description": "UPC cases ranked by relevance (most relevant first)", "items": map[string]any{ "type": "object", "properties": map[string]any{ "case_id": map[string]any{ "type": "string", "description": "The ID of the UPC case from the provided list", }, "relevance": map[string]any{ "type": "number", "minimum": 0, "maximum": 1, "description": "Relevance score from 0.0 to 1.0", }, "explanation": map[string]any{ "type": "string", "description": "Why this case is relevant — what legal issues, parties, patents, or procedural aspects are similar", }, "key_holdings": map[string]any{ "type": "string", "description": "Key holdings or legal principles from this case that are relevant", }, }, "required": []string{"case_id", "relevance", "explanation", "key_holdings"}, }, }, }, Required: []string{"cases"}, }, } const similarCaseSystemPrompt = `You are a UPC (Unified Patent Court) case law expert. Given a case description and a list of UPC cases from the database, rank the cases by relevance and explain why each one is similar or relevant. Consider: - Similar patents or technology areas - Same parties or representatives - Similar legal issues (infringement, validity, injunctions, etc.) - Similar procedural situations - Relevant legal principles that could apply Only include cases that are genuinely relevant (relevance > 0.3). Order by relevance descending.` // FindSimilarCases searches the youpc.org database for similar UPC cases. func (s *AIService) FindSimilarCases(ctx context.Context, tenantID, caseID uuid.UUID, description string) ([]SimilarCase, error) { if s.youpcDB == nil { return nil, fmt.Errorf("youpc.org database not configured") } // Build query context from the case (if provided) or description var queryText string if caseID != uuid.Nil { var c models.Case if err := s.db.GetContext(ctx, &c, "SELECT * FROM cases WHERE id = $1 AND tenant_id = $2", caseID, tenantID); err != nil { return nil, fmt.Errorf("loading case: %w", err) } var parties []models.Party _ = s.db.SelectContext(ctx, &parties, "SELECT * FROM parties WHERE case_id = $1 AND tenant_id = $2", caseID, tenantID) var b strings.Builder b.WriteString(fmt.Sprintf("Case: %s — %s\n", c.CaseNumber, c.Title)) if c.CaseType != nil { b.WriteString(fmt.Sprintf("Type: %s\n", *c.CaseType)) } if c.Court != nil { b.WriteString(fmt.Sprintf("Court: %s\n", *c.Court)) } for _, p := range parties { role := "" if p.Role != nil { role = *p.Role } b.WriteString(fmt.Sprintf("Party: %s (%s)\n", p.Name, role)) } if description != "" { b.WriteString(fmt.Sprintf("\nAdditional context: %s\n", description)) } queryText = b.String() } else if description != "" { queryText = description } else { return nil, fmt.Errorf("either case_id or description must be provided") } // Query youpc.org database for candidate cases // Search by text similarity across case titles, summaries, party names var candidates []youpcCase err := s.youpcDB.SelectContext(ctx, &candidates, ` SELECT id, case_number, title, court, decision_date, case_type, outcome, patent_numbers, summary, claimant, defendant FROM mlex.cases ORDER BY decision_date DESC NULLS LAST LIMIT 50 `) if err != nil { return nil, fmt.Errorf("querying youpc.org cases: %w", err) } if len(candidates) == 0 { return []SimilarCase{}, nil } // Build candidate list for Claude var candidateText strings.Builder for _, c := range candidates { candidateText.WriteString(fmt.Sprintf("ID: %s\n", c.ID)) if c.CaseNumber != nil { candidateText.WriteString(fmt.Sprintf("Case Number: %s\n", *c.CaseNumber)) } if c.Title != nil { candidateText.WriteString(fmt.Sprintf("Title: %s\n", *c.Title)) } if c.Court != nil { candidateText.WriteString(fmt.Sprintf("Court: %s\n", *c.Court)) } if c.DecisionDate != nil { candidateText.WriteString(fmt.Sprintf("Decision Date: %s\n", *c.DecisionDate)) } if c.CaseType != nil { candidateText.WriteString(fmt.Sprintf("Type: %s\n", *c.CaseType)) } if c.Outcome != nil { candidateText.WriteString(fmt.Sprintf("Outcome: %s\n", *c.Outcome)) } if c.PatentNumbers != nil { candidateText.WriteString(fmt.Sprintf("Patents: %s\n", *c.PatentNumbers)) } if c.Claimant != nil { candidateText.WriteString(fmt.Sprintf("Claimant: %s\n", *c.Claimant)) } if c.Defendant != nil { candidateText.WriteString(fmt.Sprintf("Defendant: %s\n", *c.Defendant)) } if c.Summary != nil { candidateText.WriteString(fmt.Sprintf("Summary: %s\n", *c.Summary)) } candidateText.WriteString("---\n") } prompt := fmt.Sprintf(`Find UPC cases relevant to this matter: %s Here are the UPC cases from the database to evaluate: %s Rank only the genuinely relevant cases by similarity.`, queryText, candidateText.String()) msg, err := s.client.Messages.New(ctx, anthropic.MessageNewParams{ Model: anthropic.ModelClaudeSonnet4_20250514, MaxTokens: 4096, System: []anthropic.TextBlockParam{ {Text: similarCaseSystemPrompt}, }, Messages: []anthropic.MessageParam{ anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)), }, Tools: []anthropic.ToolUnionParam{ {OfTool: &similarCaseTool}, }, ToolChoice: anthropic.ToolChoiceParamOfTool("rank_similar_cases"), }) if err != nil { return nil, fmt.Errorf("claude API call: %w", err) } for _, block := range msg.Content { if block.Type == "tool_use" && block.Name == "rank_similar_cases" { var input similarCaseToolInput if err := json.Unmarshal(block.Input, &input); err != nil { return nil, fmt.Errorf("parsing similar cases output: %w", err) } // Build lookup map for candidate data candidateMap := make(map[string]youpcCase) for _, c := range candidates { candidateMap[c.ID] = c } var results []SimilarCase for _, ranked := range input.Cases { if ranked.Relevance < 0.3 { continue } c, ok := candidateMap[ranked.CaseID] if !ok { continue } sc := SimilarCase{ Relevance: ranked.Relevance, Explanation: ranked.Explanation, KeyHoldings: ranked.KeyHoldings, } if c.CaseNumber != nil { sc.CaseNumber = *c.CaseNumber } if c.Title != nil { sc.Title = *c.Title } if c.Court != nil { sc.Court = *c.Court } if c.DecisionDate != nil { sc.Date = *c.DecisionDate } if c.CaseNumber != nil { sc.URL = fmt.Sprintf("https://youpc.org/cases/%s", *c.CaseNumber) } results = append(results, sc) } return results, nil } } return nil, fmt.Errorf("no tool_use block in response") }