44 "context"
55 "database/sql"
66 "fmt"
7+ "regexp"
78 "strings"
89 "time"
910)
@@ -13,15 +14,42 @@ type NamedCount struct {
1314 Count int
1415}
1516
17+ type ReportSnippet struct {
18+ Seq int
19+ Title string
20+ Summary string
21+ Classification string
22+ SeverityLevel float64
23+ UpdatedAt time.Time
24+ }
25+
1626type IntelligenceContext struct {
17- OrgID string
18- ReportsAnalyzed int
19- ReportsThisMonth int
20- HighPriorityCount int
21- MediumPriorityCount int
22- TopClassifications []NamedCount
23- TopIssues []NamedCount
24- RecentSummaries []string
27+ OrgID string
28+ ReportsAnalyzed int
29+ ReportsThisMonth int
30+ ReportsLast30Days int
31+ ReportsLast7Days int
32+ ReportsPrev7Days int
33+ GrowthLast7VsPrev7 float64
34+ HighPriorityCount int
35+ MediumPriorityCount int
36+ TopClassifications []NamedCount
37+ TopIssues []NamedCount
38+ RecentSummaries []string
39+ RepresentativeReports []ReportSnippet
40+ MatchedReports []ReportSnippet
41+ Keywords []string
42+ }
43+
44+ var keywordSplitRegex = regexp .MustCompile (`[^\p{L}\p{N}]+` )
45+
46+ var intelligenceStopWords = map [string ]struct {}{
47+ "what" : {}, "which" : {}, "this" : {}, "that" : {}, "with" : {}, "from" : {}, "about" : {},
48+ "show" : {}, "give" : {}, "please" : {}, "could" : {}, "would" : {}, "there" : {}, "their" : {},
49+ "are" : {}, "is" : {}, "the" : {}, "and" : {}, "for" : {}, "you" : {}, "our" : {}, "your" : {},
50+ "have" : {}, "has" : {}, "were" : {}, "been" : {}, "into" : {}, "over" : {}, "under" : {},
51+ "most" : {}, "least" : {}, "risks" : {}, "risk" : {}, "issues" : {}, "issue" : {}, "reports" : {},
52+ "problem" : {}, "problems" : {}, "month" : {}, "week" : {}, "today" : {}, "recent" : {},
2553}
2654
2755func (d * Database ) EnsureIntelligenceTables (ctx context.Context ) error {
@@ -103,7 +131,7 @@ func (d *Database) GetAndIncrementIntelligenceUsage(ctx context.Context, session
103131 return true , turnsUsed , nil
104132}
105133
106- func (d * Database ) GetIntelligenceContext (ctx context.Context , orgID string ) (* IntelligenceContext , error ) {
134+ func (d * Database ) GetIntelligenceContext (ctx context.Context , orgID , question string ) (* IntelligenceContext , error ) {
107135 org := strings .ToLower (strings .TrimSpace (orgID ))
108136 if org == "" {
109137 return nil , fmt .Errorf ("org_id is required" )
@@ -122,22 +150,36 @@ func (d *Database) GetIntelligenceContext(ctx context.Context, orgID string) (*I
122150 }
123151
124152 _ = d .db .QueryRowContext (ctx , `
125- SELECT COUNT(*)
126- FROM reports r
127- INNER JOIN report_analysis ra ON ra.seq = r.seq
153+ SELECT
154+ COALESCE(SUM(CASE WHEN r.ts >= DATE_FORMAT(UTC_TIMESTAMP(), '%Y-%m-01 00:00:00') THEN 1 ELSE 0 END), 0) AS month_count,
155+ COALESCE(SUM(CASE WHEN r.ts >= UTC_TIMESTAMP() - INTERVAL 30 DAY THEN 1 ELSE 0 END), 0) AS last_30d_count,
156+ COALESCE(SUM(CASE WHEN r.ts >= UTC_TIMESTAMP() - INTERVAL 7 DAY THEN 1 ELSE 0 END), 0) AS last_7d_count,
157+ COALESCE(SUM(CASE WHEN r.ts < UTC_TIMESTAMP() - INTERVAL 7 DAY AND r.ts >= UTC_TIMESTAMP() - INTERVAL 14 DAY THEN 1 ELSE 0 END), 0) AS prev_7d_count
158+ FROM report_analysis ra
159+ INNER JOIN reports r ON ra.seq = r.seq
128160 WHERE ra.brand_name = ?
129161 AND ra.is_valid = TRUE
130- AND r.ts >= DATE_FORMAT(UTC_TIMESTAMP(), '%Y-%m-01 00:00:00')
131- ` , org ).Scan (& result .ReportsThisMonth )
162+ ` , org ).Scan (
163+ & result .ReportsThisMonth ,
164+ & result .ReportsLast30Days ,
165+ & result .ReportsLast7Days ,
166+ & result .ReportsPrev7Days ,
167+ )
168+
169+ if result .ReportsPrev7Days > 0 {
170+ result .GrowthLast7VsPrev7 = (float64 (result .ReportsLast7Days - result .ReportsPrev7Days ) / float64 (result .ReportsPrev7Days )) * 100.0
171+ } else if result .ReportsLast7Days > 0 {
172+ result .GrowthLast7VsPrev7 = 100.0
173+ }
132174
133175 classRows , err := d .db .QueryContext (ctx , `
134- SELECT COALESCE(classification, 'unknown') AS classification, COUNT(*) AS cnt
176+ SELECT COALESCE(NULLIF( classification, '') , 'unknown') AS classification, COUNT(*) AS cnt
135177 FROM report_analysis
136178 WHERE brand_name = ?
137179 AND is_valid = TRUE
138180 GROUP BY classification
139181 ORDER BY cnt DESC
140- LIMIT 5
182+ LIMIT 6
141183 ` , org )
142184 if err == nil {
143185 defer classRows .Close ()
@@ -158,7 +200,7 @@ func (d *Database) GetIntelligenceContext(ctx context.Context, orgID string) (*I
158200 AND title != ''
159201 GROUP BY title
160202 ORDER BY cnt DESC
161- LIMIT 5
203+ LIMIT 6
162204 ` , org )
163205 if err == nil {
164206 defer issueRows .Close ()
@@ -178,7 +220,7 @@ func (d *Database) GetIntelligenceContext(ctx context.Context, orgID string) (*I
178220 AND summary IS NOT NULL
179221 AND summary != ''
180222 ORDER BY created_at DESC
181- LIMIT 5
223+ LIMIT 8
182224 ` , org )
183225 if err == nil {
184226 defer summaryRows .Close ()
@@ -190,5 +232,129 @@ func (d *Database) GetIntelligenceContext(ctx context.Context, orgID string) (*I
190232 }
191233 }
192234
235+ representative , repErr := d .getReportSnippets (ctx , org , nil , 6 )
236+ if repErr == nil {
237+ result .RepresentativeReports = representative
238+ }
239+
240+ keywords := extractKeywords (question )
241+ result .Keywords = keywords
242+ if len (keywords ) > 0 {
243+ matched , matchErr := d .getReportSnippets (ctx , org , keywords , 5 )
244+ if matchErr == nil {
245+ result .MatchedReports = mergeUniqueSnippets (matched , representative , 5 )
246+ }
247+ }
248+
193249 return result , nil
194250}
251+
252+ func (d * Database ) getReportSnippets (ctx context.Context , org string , keywords []string , limit int ) ([]ReportSnippet , error ) {
253+ if limit <= 0 {
254+ limit = 3
255+ }
256+
257+ query := `
258+ SELECT
259+ ra.seq,
260+ COALESCE(NULLIF(ra.title, ''), '(untitled report)') AS title,
261+ COALESCE(NULLIF(ra.summary, ''), COALESCE(NULLIF(ra.description, ''), '(no summary available)')) AS summary,
262+ COALESCE(NULLIF(ra.classification, ''), 'unknown') AS classification,
263+ COALESCE(ra.severity_level, 0) AS severity_level,
264+ COALESCE(ra.updated_at, ra.created_at, r.ts) AS updated_at
265+ FROM report_analysis ra
266+ INNER JOIN reports r ON r.seq = ra.seq
267+ WHERE ra.brand_name = ?
268+ AND ra.is_valid = TRUE
269+ `
270+
271+ args := make ([]interface {}, 0 , 1 + len (keywords )* 3 + 1 )
272+ args = append (args , org )
273+
274+ if len (keywords ) > 0 {
275+ clauses := make ([]string , 0 , len (keywords ))
276+ for _ , kw := range keywords {
277+ clauses = append (clauses , `(LOWER(ra.title) LIKE ? OR LOWER(ra.summary) LIKE ? OR LOWER(ra.description) LIKE ?)` )
278+ pattern := "%" + strings .ToLower (strings .TrimSpace (kw )) + "%"
279+ args = append (args , pattern , pattern , pattern )
280+ }
281+ query += " AND (" + strings .Join (clauses , " OR " ) + ")"
282+ }
283+
284+ query += `
285+ ORDER BY ra.severity_level DESC, updated_at DESC
286+ LIMIT ?
287+ `
288+ args = append (args , limit )
289+
290+ rows , err := d .db .QueryContext (ctx , query , args ... )
291+ if err != nil {
292+ return nil , err
293+ }
294+ defer rows .Close ()
295+
296+ result := make ([]ReportSnippet , 0 , limit )
297+ for rows .Next () {
298+ var item ReportSnippet
299+ if scanErr := rows .Scan (& item .Seq , & item .Title , & item .Summary , & item .Classification , & item .SeverityLevel , & item .UpdatedAt ); scanErr != nil {
300+ return nil , scanErr
301+ }
302+ result = append (result , item )
303+ }
304+ if err := rows .Err (); err != nil {
305+ return nil , err
306+ }
307+ return result , nil
308+ }
309+
310+ func extractKeywords (question string ) []string {
311+ q := strings .ToLower (strings .TrimSpace (question ))
312+ if q == "" {
313+ return nil
314+ }
315+
316+ raw := keywordSplitRegex .Split (q , - 1 )
317+ seen := make (map [string ]struct {}, len (raw ))
318+ keywords := make ([]string , 0 , 4 )
319+ for _ , token := range raw {
320+ t := strings .TrimSpace (token )
321+ if len (t ) < 4 {
322+ continue
323+ }
324+ if _ , stop := intelligenceStopWords [t ]; stop {
325+ continue
326+ }
327+ if _ , dup := seen [t ]; dup {
328+ continue
329+ }
330+ seen [t ] = struct {}{}
331+ keywords = append (keywords , t )
332+ if len (keywords ) == 4 {
333+ break
334+ }
335+ }
336+ return keywords
337+ }
338+
339+ func mergeUniqueSnippets (primary , secondary []ReportSnippet , max int ) []ReportSnippet {
340+ if max <= 0 {
341+ max = 3
342+ }
343+ out := make ([]ReportSnippet , 0 , max )
344+ seen := make (map [int ]struct {}, max )
345+ appendUnique := func (items []ReportSnippet ) {
346+ for _ , item := range items {
347+ if len (out ) >= max {
348+ return
349+ }
350+ if _ , exists := seen [item .Seq ]; exists {
351+ continue
352+ }
353+ seen [item .Seq ] = struct {}{}
354+ out = append (out , item )
355+ }
356+ }
357+ appendUnique (primary )
358+ appendUnique (secondary )
359+ return out
360+ }
0 commit comments