Add LLM-based patent classification tagging by technology domain

- Add classify_patent_tags() to LLMAnalyzer with canonical tag list
  (ai, semiconductors, materials, biotech, networking, other)
- Add patent_tags TEXT[] column to patents table with GIN index
- Run classification automatically in the analysis pipeline after
  patent processing; persist tags via update_patent_tags()
- Include tags in CompanyAnalysisResult and API response models
- Add ?tags= filter to GET /analyze/batch endpoint
- Add GET /analytics/tags endpoint for tag distribution data
- Add tag filter controls and distribution chart to Analytics page
- Add 12 unit tests covering classification, DB storage, and caching

Closes leeworks-agents/SPARC#1672

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
agent-company
2026-05-19 15:27:46 +00:00
parent 313800215c
commit cd81218154
8 changed files with 590 additions and 17 deletions
+10
View File
@@ -199,8 +199,18 @@ export const analyticsApi = {
const response = await api.get<TrendData>(`/analytics/trends?days=${days}`);
return response.data;
},
getTagDistribution: async (): Promise<TagDistribution> => {
const response = await api.get<TagDistribution>('/analytics/tags');
return response.data;
},
};
export interface TagDistribution {
by_tag: Array<{ tag: string; count: number }>;
canonical_tags: string[];
}
// Admin API
export const adminApi = {
listUsers: async (limit = 100, offset = 0): Promise<User[]> => {
+95 -1
View File
@@ -1,14 +1,24 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { analyticsApi } from '../api/client';
import { AlertCircle, Database } from 'lucide-react';
import { AlertCircle, Database, Tag } from 'lucide-react';
import { PieChart, Pie, Cell, BarChart, Bar, LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend } from 'recharts';
import { useChartTheme } from '../context/useChartTheme';
const COLORS = ['#6366f1', '#0ea5e9', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#14b8a6'];
const TAG_COLORS: Record<string, string> = {
ai: '#6366f1',
semiconductors: '#0ea5e9',
materials: '#10b981',
biotech: '#f59e0b',
networking: '#ec4899',
other: '#8b5cf6',
};
export function AnalyticsPage() {
const [days, setDays] = useState(30);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const chartTheme = useChartTheme();
const { data, isLoading, isError, refetch } = useQuery({
@@ -21,6 +31,17 @@ export function AnalyticsPage() {
queryFn: () => analyticsApi.getTrends(days),
});
const tagQuery = useQuery({
queryKey: ['analytics-tags'],
queryFn: () => analyticsApi.getTagDistribution(),
});
const toggleTag = (tag: string) => {
setSelectedTags((prev) =>
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
);
};
if (isLoading) {
return (
<div className="space-y-6">
@@ -107,6 +128,13 @@ export function AnalyticsPage() {
count: t.count,
}));
const tagData = tagQuery.data?.by_tag?.map((t) => ({
name: t.tag,
count: t.count,
})) || [];
const canonicalTags = tagQuery.data?.canonical_tags || [];
return (
<div className="space-y-6">
{/* Header */}
@@ -131,6 +159,50 @@ export function AnalyticsPage() {
</select>
</div>
{/* Tag Filter Controls */}
{canonicalTags.length > 0 && (
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-4">
<div className="flex items-center gap-2 mb-3">
<Tag size={16} className="text-primary" />
<span className="text-sm font-semibold text-text-primary">Filter by Technology Domain</span>
{selectedTags.length > 0 && (
<button
onClick={() => setSelectedTags([])}
className="ml-auto text-xs text-text-secondary hover:text-primary transition-colors"
>
Clear all
</button>
)}
</div>
<div className="flex flex-wrap gap-2">
{canonicalTags.map((tag) => {
const isActive = selectedTags.includes(tag);
const color = TAG_COLORS[tag] || '#8b5cf6';
const tagCount = tagQuery.data?.by_tag?.find((t) => t.tag === tag)?.count || 0;
return (
<button
key={tag}
onClick={() => toggleTag(tag)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
isActive
? 'text-white shadow-md'
: 'bg-bg-card/80 text-text-secondary border border-primary/20 hover:border-primary/40'
}`}
style={isActive ? { backgroundColor: color } : {}}
>
{tag}
{tagCount > 0 && (
<span className={`ml-1.5 text-xs ${isActive ? 'opacity-80' : 'opacity-60'}`}>
({tagCount})
</span>
)}
</button>
);
})}
</div>
</div>
)}
{/* Summary Metrics */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<MetricCard label="Total Analyses" value={data.total_messages} />
@@ -187,6 +259,28 @@ export function AnalyticsPage() {
</ResponsiveContainer>
</div>
)}
{/* Bar Chart - Technology Domain Tags */}
{tagData.length > 0 && (
<div className="bg-bg-card/60 border border-primary/15 rounded-2xl p-6">
<h3 className="text-lg font-semibold text-text-primary mb-4">Patents by Technology Domain</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={tagData}>
<XAxis dataKey="name" stroke={chartTheme.axisStroke} fontSize={12} />
<YAxis stroke={chartTheme.axisStroke} fontSize={12} />
<Tooltip
contentStyle={chartTheme.tooltipContentStyle}
labelStyle={chartTheme.tooltipLabelStyle}
/>
<Bar dataKey="count" radius={[4, 4, 0, 0]}>
{tagData.map((entry, index) => (
<Cell key={`tag-cell-${index}`} fill={TAG_COLORS[entry.name] || COLORS[index % COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
{/* Trend Charts */}