webctx 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ package app
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "os"
8
+ "sync"
9
+ "time"
10
+ )
11
+
12
+ type localPaidReplay struct {
13
+ fingerprint string
14
+ output map[string]any
15
+ expires time.Time
16
+ }
17
+
18
+ var paidReplayMemory = struct {
19
+ sync.Mutex
20
+ items map[string]localPaidReplay
21
+ }{items: map[string]localPaidReplay{}}
22
+
23
+ // In a single warm worker, serialize requests with the same idempotency key.
24
+ // Without this, two concurrent requests can both miss the replay cache and
25
+ // execute before either writes the receipt.
26
+ type paidReplayGate struct {
27
+ mu sync.Mutex
28
+ users int
29
+ }
30
+
31
+ var paidReplayGates = struct {
32
+ sync.Mutex
33
+ items map[string]*paidReplayGate
34
+ }{items: map[string]*paidReplayGate{}}
35
+
36
+ func lockPaidRequest(id string) func() {
37
+ key := localReplayKey(id)
38
+ paidReplayGates.Lock()
39
+ gate := paidReplayGates.items[key]
40
+ if gate == nil {
41
+ gate = &paidReplayGate{}
42
+ paidReplayGates.items[key] = gate
43
+ }
44
+ gate.users++
45
+ paidReplayGates.Unlock()
46
+ gate.mu.Lock()
47
+ return func() {
48
+ gate.mu.Unlock()
49
+ paidReplayGates.Lock()
50
+ gate.users--
51
+ if gate.users == 0 {
52
+ delete(paidReplayGates.items, key)
53
+ }
54
+ paidReplayGates.Unlock()
55
+ }
56
+ }
57
+
58
+ func localReplayKey(id string) string {
59
+ h := sha256.Sum256([]byte(os.Getenv("FIRECRAWL_API_KEY") + "\x00" + id))
60
+ return hex.EncodeToString(h[:])
61
+ }
62
+ func paidRequestFingerprint(calls []DataCall) string {
63
+ body, _ := json.Marshal(calls)
64
+ h := sha256.Sum256(body)
65
+ return hex.EncodeToString(h[:])
66
+ }
67
+
68
+ // This ledger is only a warm-worker replay optimization. It cannot claim to
69
+ // verify charges or prevent replay across multiple Vercel instances; upstream
70
+ // x-request-id idempotency remains authoritative in that situation.
71
+ func cachedPaidReplay(requestID string, calls []DataCall) (map[string]any, error) {
72
+ key := localReplayKey(requestID)
73
+ fingerprint := paidRequestFingerprint(calls)
74
+ paidReplayMemory.Lock()
75
+ entry, ok := paidReplayMemory.items[key]
76
+ paidReplayMemory.Unlock()
77
+ if !ok || time.Now().After(entry.expires) {
78
+ return nil, nil
79
+ }
80
+ if entry.fingerprint != fingerprint {
81
+ return nil, &DataError{
82
+ Code: "duplicate_request",
83
+ Message: "This request_id already completed with different inputs. Restore the original calls to replay, or omit request_id for a new query.",
84
+ Details: map[string]any{"recovery": "Never retry different inputs with the same ID. Use the original operation and input list for replay, or generate a fresh ID."},
85
+ }
86
+ }
87
+ raw, err := json.Marshal(entry.output)
88
+ if err != nil {
89
+ return nil, nil
90
+ }
91
+ var result map[string]any
92
+ if json.Unmarshal(raw, &result) != nil {
93
+ return nil, nil
94
+ }
95
+ result["replayed"] = true
96
+ result["replay_status"] = "served_from_local_cache"
97
+ result["original_credits_used"] = result["credits_used"]
98
+ result["credits_charged_this_request"] = 0
99
+ result["billing_note"] = "This exact replay was served from Origo's in-process cache without contacting Firecrawl, so Origo incurred no new upstream execution on this attempt."
100
+ return result, nil
101
+ }
102
+
103
+ func storePaidReplay(requestID string, calls []DataCall, output map[string]any) {
104
+ raw, err := json.Marshal(output)
105
+ if err != nil || len(raw) > 1<<20 {
106
+ return
107
+ }
108
+ key := localReplayKey(requestID)
109
+ paidReplayMemory.Lock()
110
+ defer paidReplayMemory.Unlock()
111
+ if len(paidReplayMemory.items) > 192 {
112
+ paidReplayMemory.items = map[string]localPaidReplay{}
113
+ }
114
+ paidReplayMemory.items[key] = localPaidReplay{
115
+ fingerprint: paidRequestFingerprint(calls), output: output,
116
+ expires: time.Now().Add(15 * time.Minute),
117
+ }
118
+ }
@@ -1,6 +1,7 @@
1
1
  package app
2
2
 
3
3
  import (
4
+ "net/url"
4
5
  "sort"
5
6
  "strings"
6
7
  )
@@ -60,7 +61,7 @@ func normalizeResearchInput(input ResearchDataInput) (ResearchDataInput, string,
60
61
  if err != nil {
61
62
  return input, "", nil, err
62
63
  }
63
- if input.Limit == 0 {
64
+ if input.Limit == 0 && !input.limitProvided {
64
65
  input.Limit = 10
65
66
  }
66
67
  if input.Limit < 1 || input.Limit > 100 {
@@ -69,11 +70,27 @@ func normalizeResearchInput(input ResearchDataInput) (ResearchDataInput, string,
69
70
  if input.Offset < 0 || input.Offset > 100000 {
70
71
  return input, "", nil, dataError("invalid_offset", "offset must be between 0 and 100000.")
71
72
  }
73
+ if input.Mode == "ranked" && input.Offset > 0 && strings.TrimSpace(input.Query) != "" &&
74
+ len(input.Sources) == 0 && len(input.URLs) == 0 && len(input.Groups) == 0 &&
75
+ len(input.Categories) == 0 && len(input.Operations) == 0 {
76
+ return input, "", nil, dataError("ranked_search_not_pageable",
77
+ "Ranked semantic search has no native pagination cursor. Use mode=catalogue with offset, or supply a source/category filter to browse and rerank catalogue pages.")
78
+ }
72
79
  for _, target := range input.URLs {
73
80
  if err := validateSourceURL(target); err != nil {
74
81
  return input, "", nil, dataError("invalid_url", "Website filters require public HTTP(S) URLs.")
75
82
  }
76
83
  }
84
+ if len(input.URLs) > 0 && len(input.Sources) == 0 {
85
+ for _, target := range input.URLs {
86
+ u, _ := url.Parse(target)
87
+ host := strings.TrimPrefix(strings.ToLower(u.Hostname()), "www.")
88
+ if host != "" {
89
+ source := strings.ReplaceAll(host, ".", "-")
90
+ input.Sources = append(input.Sources, source)
91
+ }
92
+ }
93
+ }
77
94
  return input, view, expand, nil
78
95
  }
79
96
 
@@ -82,7 +99,7 @@ func nativeBrowseOptions(input ResearchDataInput, view string, expand []string)
82
99
  if input.Query != "" {
83
100
  o["query"] = input.Query
84
101
  }
85
- if len(input.URLs) > 0 {
102
+ if len(input.URLs) > 0 && len(input.Sources) == 0 {
86
103
  o["urls"] = input.URLs
87
104
  }
88
105
  if len(input.Sources) > 0 {
@@ -123,6 +140,9 @@ func nativeBrowseOptions(input ResearchDataInput, view string, expand []string)
123
140
  }
124
141
  }
125
142
  if len(expand) > 0 {
143
+ if containsString(expand, "examples") && !containsString(expand, "options") {
144
+ expand = append(append([]string(nil), expand...), "options")
145
+ }
126
146
  o["expand"] = expand
127
147
  }
128
148
  return o
@@ -145,9 +165,19 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
145
165
  }
146
166
  ctx, cancel := defaultDataContext()
147
167
  defer cancel()
168
+ explicitExact := len(input.Operations) > 0
169
+ // An exact operation ID is authoritative. A descriptive query helps
170
+ // communicate intent, but must not exclude a known operation.
171
+ note := ""
172
+ if explicitExact && input.Query != "" {
173
+ note = "Exact operation IDs take precedence over descriptive query terms; the query was not used as a restrictive upstream filter."
174
+ input.Query = ""
175
+ }
148
176
  semantic := input.Mode != "catalogue" && input.Query != "" && view == "tools" && input.Offset == 0 &&
149
177
  len(input.URLs) == 0 && len(input.Sources) == 0 && len(input.Categories) == 0 &&
150
178
  len(input.Groups) == 0 && len(input.Operations) == 0
179
+ localFilteredRanking := view == "tools" && input.Query != "" &&
180
+ (len(input.URLs) > 0 || len(input.Sources) > 0 || len(input.Groups) > 0 || len(input.Categories) > 0)
151
181
  var items []any
152
182
  var total any
153
183
  var next any
@@ -159,9 +189,16 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
159
189
  // compact upstream matches may omit both price and operation name.
160
190
  // We still return compact result cards unless include was requested.
161
191
  detail := "full"
192
+ fetchLimit := input.Limit * 4
193
+ if fetchLimit < 20 {
194
+ fetchLimit = 20
195
+ }
196
+ if fetchLimit > 100 {
197
+ fetchLimit = 100
198
+ }
162
199
  raw, err = doDataAPI(ctx, "/search", map[string]any{
163
200
  "query": input.Query, "sources": []string{"alexandria"},
164
- "limit": input.Limit, "toolDetail": detail,
201
+ "limit": fetchLimit, "toolDetail": detail,
165
202
  }, "")
166
203
  if err != nil {
167
204
  return nil, err
@@ -171,17 +208,6 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
171
208
  // Some upstream ranked matches arrive out of order despite including a
172
209
  // similarity score. Present the highest-scoring candidate first, retaining
173
210
  // upstream order for exact ties or missing scores.
174
- sort.SliceStable(items, func(i, j int) bool {
175
- a, aok := numberField(mapField(items[i])["similarity"])
176
- b, bok := numberField(mapField(items[j])["similarity"])
177
- if aok != bok {
178
- return aok
179
- }
180
- if !aok {
181
- return false
182
- }
183
- return a > b
184
- })
185
211
  // Ranked search has no native cursor. Expose the free, complete
186
212
  // catalogue with matching semantic query as an optional expansion.
187
213
  next = map[string]any{"tool": "research", "arguments": map[string]any{
@@ -189,6 +215,24 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
189
215
  }, "note": "Browse all matching operations with pagination; ranked discovery itself has no cursor."}
190
216
  } else {
191
217
  options := nativeBrowseOptions(input, view, expand)
218
+ if localFilteredRanking {
219
+ // Provider-specified lexical filters can exclude operations whose
220
+ // descriptions describe the requested data indirectly. Fetch a
221
+ // catalogue page from the actual source, then rank that page.
222
+ // Do not reset every continuation to offset zero: large sources
223
+ // can have hundreds of operations.
224
+ delete(options, "query")
225
+ options["limit"] = 100
226
+ options["offset"] = (input.Offset / 100) * 100
227
+ note = "Source/category filters select candidates first. The question ranks each catalogue page locally (up to 100 operations per page); this is not a global ranking across unvisited pages."
228
+ if input.Mode != "catalogue" {
229
+ mode = "ranked_filtered"
230
+ }
231
+ }
232
+ if explicitExact {
233
+ delete(options, "query")
234
+ delete(options, "urls")
235
+ }
192
236
  var catalogue map[string]any
193
237
  catalogue, raw, err = freeDataBrowse(ctx, options)
194
238
  if err != nil {
@@ -200,6 +244,52 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
200
244
  next = continuationFromCatalogue(native)
201
245
  }
202
246
  }
247
+ if view == "tools" {
248
+ termsQuery := input.Query
249
+ for _, op := range input.Operations {
250
+ termsQuery += " " + op
251
+ }
252
+ if strings.TrimSpace(termsQuery) != "" {
253
+ sort.SliceStable(items, func(i, j int) bool {
254
+ return intentRelevance(mapField(items[i]), termsQuery) > intentRelevance(mapField(items[j]), termsQuery)
255
+ })
256
+ } else {
257
+ sort.SliceStable(items, func(i, j int) bool {
258
+ a, aok := numberField(mapField(items[i])["similarity"])
259
+ b, bok := numberField(mapField(items[j])["similarity"])
260
+ if aok != bok {
261
+ return aok
262
+ }
263
+ return a > b
264
+ })
265
+ }
266
+ }
267
+ if semantic && len(items) > input.Limit {
268
+ items = items[:input.Limit]
269
+ }
270
+ if localFilteredRanking {
271
+ count := len(items)
272
+ start := input.Offset % 100
273
+ if start > count {
274
+ start = count
275
+ }
276
+ end := start + input.Limit
277
+ if end > count {
278
+ end = count
279
+ }
280
+ items = items[start:end]
281
+ sourceTotal, hasTotal := numberField(total)
282
+ more := count > end || (hasTotal && sourceTotal > float64((input.Offset/100)*100+count))
283
+ if more {
284
+ next = map[string]any{"tool": "research", "arguments": ResearchDataInput{
285
+ Query: input.Query, Mode: input.Mode, URLs: input.URLs, Sources: input.Sources,
286
+ Categories: input.Categories, Groups: input.Groups, View: niceDataView(view),
287
+ Include: input.Include, Limit: input.Limit, Offset: input.Offset + len(items),
288
+ }}
289
+ } else {
290
+ next = nil
291
+ }
292
+ }
203
293
  results := make([]map[string]any, 0, len(items))
204
294
  for _, value := range items {
205
295
  entry := mapField(value)
@@ -223,6 +313,15 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
223
313
  }
224
314
  if cost, ok := entry["creditsCost"]; ok {
225
315
  result["credits_per_call"] = cost
316
+ if listed, ok := numberField(cost); ok {
317
+ if listed > float64(dataCreditCap()) {
318
+ result["executable"] = false
319
+ result["availability"] = "exceeds_server_credit_limit"
320
+ result["server_credit_limit"] = dataCreditCap()
321
+ } else if view == "tools" {
322
+ result["executable"] = true
323
+ }
324
+ }
226
325
  }
227
326
  if cost, ok := entry["perRecord"]; ok {
228
327
  result["per_record"] = cost
@@ -230,8 +329,14 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
230
329
  if count, ok := entry["toolCount"]; ok {
231
330
  result["operation_count"] = count
232
331
  }
332
+ if input.Query != "" && view == "tools" {
333
+ result["relevance"] = intentRelevance(entry, input.Query)
334
+ }
233
335
  if rank, ok := entry["similarity"]; ok {
234
- result["relevance"] = rank
336
+ result["semantic_similarity"] = rank
337
+ if input.Query == "" {
338
+ result["relevance"] = rank
339
+ }
235
340
  }
236
341
  if len(expand) > 0 {
237
342
  if selected := entry["options"]; selected != nil && containsString(expand, "options") {
@@ -243,8 +348,15 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
243
348
  if selected := entry["examples"]; selected != nil && containsString(expand, "examples") {
244
349
  result["examples"] = selected
245
350
  }
351
+ if containsString(expand, "examples") && result["examples"] == nil {
352
+ if entry["options"] != nil {
353
+ result["examples"] = contractExamples(entry)
354
+ }
355
+ }
246
356
  }
247
- if continuation := mapField(entry["next"]); continuation != nil {
357
+ if view == "tools" && id != "" {
358
+ result["next"] = map[string]any{"tool": "inspect", "arguments": map[string]any{"id": id}}
359
+ } else if continuation := mapField(entry["next"]); continuation != nil {
248
360
  result["next"] = continuationFromCatalogue(continuation)
249
361
  } else if id != "" {
250
362
  result["next"] = map[string]any{"tool": "inspect", "arguments": map[string]any{"id": id}}
@@ -256,6 +368,18 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
256
368
  "count": len(results), "cost_credits": 0,
257
369
  "guidance": "Use inspect with an operation ID to see complete inputs, output, examples and the next execute call. To explore a source, inspect its source ID.",
258
370
  }
371
+ if note != "" {
372
+ output["mode_note"] = note
373
+ }
374
+ if localFilteredRanking {
375
+ output["ranking_scope"] = "catalogue_page"
376
+ if sourceTotal, ok := numberField(total); ok {
377
+ output["ranking_incomplete"] = sourceTotal > 100
378
+ }
379
+ }
380
+ if len(input.URLs) > 0 && len(input.Sources) > 0 {
381
+ output["resolved_url_sources"] = input.Sources
382
+ }
259
383
  if total != nil {
260
384
  output["total"] = total
261
385
  }
@@ -268,6 +392,55 @@ func ResearchData(input ResearchDataInput) (map[string]any, error) {
268
392
  return output, nil
269
393
  }
270
394
 
395
+ // Rank operations by their ability to deliver the requested data, not just
396
+ // semantic similarity. Keep the provider similarity separately for auditing.
397
+ func intentRelevance(entry map[string]any, query string) float64 {
398
+ if entry == nil {
399
+ return 0
400
+ }
401
+ base, _ := numberField(entry["similarity"])
402
+ if query == "" {
403
+ return base
404
+ }
405
+ q := strings.ToLower(query)
406
+ id := strings.ToLower(stringField(entry["id"]))
407
+ name := strings.ToLower(stringField(entry["name"]))
408
+ desc := strings.ToLower(stringField(entry["description"]))
409
+ p := id + " " + name
410
+ searchIntent := strings.Contains(q, "search") || strings.Contains(q, "find") ||
411
+ strings.Contains(q, "papers") || strings.Contains(q, "podcast") ||
412
+ strings.Contains(q, "trials") || strings.Contains(q, "about")
413
+ historyIntent := strings.Contains(q, "historical") || strings.Contains(q, "history") ||
414
+ strings.Contains(q, "time series") || strings.Contains(q, "over time") ||
415
+ strings.Contains(q, "inflation")
416
+ if historyIntent && (strings.Contains(p, "observations") ||
417
+ strings.Contains(p, "history") || strings.Contains(p, "rates") ||
418
+ strings.Contains(p, "timeseries")) {
419
+ base += 0.23
420
+ }
421
+ if searchIntent && (strings.Contains(p, "/search") || strings.Contains(p, "/studies") ||
422
+ strings.Contains(p, "episode-search")) {
423
+ base += 0.20
424
+ }
425
+ if searchIntent && strings.Contains(p, "/list") && !strings.Contains(q, "list") {
426
+ base -= 0.04
427
+ }
428
+ for _, ancillary := range []string{"suggest", "related", "directory", "releases", "/source", "metadata", "browse"} {
429
+ if strings.Contains(p, ancillary) && !strings.Contains(q, strings.TrimLeft(ancillary, "/")) {
430
+ base -= 0.14
431
+ }
432
+ }
433
+ terms := queryTerms(query)
434
+ if len(terms) > 0 {
435
+ fit := termScore(name+" "+desc, terms)
436
+ if fit > 8 {
437
+ fit = 8
438
+ }
439
+ base += float64(fit) * 0.007
440
+ }
441
+ return float64(int(base*1000000+0.5)) / 1000000
442
+ }
443
+
271
444
  func niceDataView(view string) string {
272
445
  switch view {
273
446
  case "providers":
@@ -286,8 +286,8 @@ func TestExecuteHandlesProviderTermsWithoutAccepting(t *testing.T) {
286
286
  mapField(typed.RequiresAction)["url"] != "https://firecrawl.dev/app/settings?tab=data-sources" {
287
287
  t.Fatalf("terms condition lost: %+v", err)
288
288
  }
289
- if mapField(typed.Details)["request_id"] == nil {
290
- t.Fatalf("lost retriable request identifier: %+v", typed)
289
+ if mapField(typed.Details)["retry"] != nil {
290
+ t.Fatalf("terms rejection must not recommend automatic paid retry: %+v", typed)
291
291
  }
292
292
  }
293
293
 
@@ -1,20 +1,43 @@
1
1
  package app
2
2
 
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ )
7
+
3
8
  // ResearchData discovers data sources and their available operations. Search
4
9
  // and catalogue browsing are both free Firecrawl Alexandria operations.
5
10
  type ResearchDataInput struct {
6
- Query string `json:"query,omitempty" jsonschema:"Optional natural-language description of the information needed"`
7
- Mode string `json:"mode,omitempty" jsonschema:"ranked semantic discovery or catalogue browsing with filters and pagination; defaults to ranked for a query"`
8
- URLs []string `json:"urls,omitempty" jsonschema:"Filter by websites associated with a data source"`
9
- Sources []string `json:"sources,omitempty" jsonschema:"Filter by source IDs returned by research"`
10
- Categories []string `json:"categories,omitempty" jsonschema:"Filter by catalogue category IDs"`
11
- Groups []string `json:"groups,omitempty" jsonschema:"Filter by catalogue group IDs"`
12
- Operations []string `json:"operations,omitempty" jsonschema:"Filter by exact operation IDs"`
13
- View string `json:"view,omitempty" jsonschema:"What to browse: sources, groups, or operations; default operations for a query, otherwise sources"`
14
- Include []string `json:"include,omitempty" jsonschema:"Additional contract details: inputs, output, examples. Default is a concise overview"`
15
- Limit int `json:"limit,omitempty" jsonschema:"Maximum results in this page, 1-100; default 10"`
16
- Offset int `json:"offset,omitempty" jsonschema:"Catalogue pagination offset, zero-based"`
17
- Raw bool `json:"raw,omitempty" jsonschema:"Also include the complete original Firecrawl response"`
11
+ Query string `json:"query,omitempty" jsonschema:"Optional natural-language description of the information needed"`
12
+ Mode string `json:"mode,omitempty" jsonschema:"ranked semantic discovery or catalogue browsing with filters and pagination; defaults to ranked for a query"`
13
+ URLs []string `json:"urls,omitempty" jsonschema:"Filter by websites associated with a data source"`
14
+ Sources []string `json:"sources,omitempty" jsonschema:"Filter by source IDs returned by research"`
15
+ Categories []string `json:"categories,omitempty" jsonschema:"Filter by catalogue category IDs"`
16
+ Groups []string `json:"groups,omitempty" jsonschema:"Filter by catalogue group IDs"`
17
+ Operations []string `json:"operations,omitempty" jsonschema:"Filter by exact operation IDs"`
18
+ View string `json:"view,omitempty" jsonschema:"What to browse: sources, groups, or operations; default operations for a query, otherwise sources"`
19
+ Include []string `json:"include,omitempty" jsonschema:"Additional contract details: inputs, output, examples. Default is a concise overview"`
20
+ Limit int `json:"limit,omitempty" jsonschema:"Maximum results in this page, 1-100; default 10"`
21
+ Offset int `json:"offset,omitempty" jsonschema:"Catalogue pagination offset, zero-based"`
22
+ Raw bool `json:"raw,omitempty" jsonschema:"Also include the complete original Firecrawl response"`
23
+ limitProvided bool `json:"-"`
24
+ }
25
+
26
+ func (input *ResearchDataInput) UnmarshalJSON(raw []byte) error {
27
+ type noMethods ResearchDataInput
28
+ var parsed noMethods
29
+ decoder := json.NewDecoder(bytes.NewReader(raw))
30
+ decoder.DisallowUnknownFields()
31
+ if err := decoder.Decode(&parsed); err != nil {
32
+ return err
33
+ }
34
+ var fields map[string]json.RawMessage
35
+ if err := json.Unmarshal(raw, &fields); err != nil {
36
+ return err
37
+ }
38
+ *input = ResearchDataInput(parsed)
39
+ _, input.limitProvided = fields["limit"]
40
+ return nil
18
41
  }
19
42
 
20
43
  // InspectDataInput returns the exact contract for a selected operation, or
@@ -88,6 +88,9 @@ func readLinkFocused(rawURL, question string, reader func(string) (string, error
88
88
  return "", err
89
89
  }
90
90
  primary = copyableCurlExamples(primary)
91
+ if answer, ok := directlyAnsweredMetadata(rawURL, question, primary); ok {
92
+ return answer, nil
93
+ }
91
94
 
92
95
  // Combine on-page links and authoritative Markdown index links instead of
93
96
  // filling the entire budget from whichever links happen to appear first.
@@ -95,7 +98,7 @@ func readLinkFocused(rawURL, question string, reader func(string) (string, error
95
98
  // SQLite, webhook, and API references over incidental alarm/concept pages.
96
99
  links := researchCandidates(rawURL, primary, question, maxRelatedSources)
97
100
  structuredDone := make(chan string, 1)
98
- if strings.Contains(primary, "**Markdown source:**") {
101
+ if strings.Contains(primary, "**Markdown source:**") || !asksForAPIContracts(question) {
99
102
  structuredDone <- ""
100
103
  } else {
101
104
  go func() { structuredDone <- embeddedStructuredContext(rawURL, question) }()
@@ -0,0 +1,30 @@
1
+ package app
2
+
3
+ import (
4
+ "fmt"
5
+ "regexp"
6
+ "strings"
7
+ )
8
+
9
+ var githubLicenseField = regexp.MustCompile(`(?mi)^license:\s*["']?([A-Za-z0-9.+-]+)["']?\s*$`)
10
+ var repositoryLicenseURL = regexp.MustCompile(`https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/blob/[A-Za-z0-9_.-]+/(?:LICENSE|COPYING)(?:\.md|\.txt)?`)
11
+
12
+ func directlyAnsweredMetadata(rawURL, question, markdown string) (string, bool) {
13
+ q := strings.ToLower(question)
14
+ if !strings.Contains(q, "license") || !strings.Contains(strings.ToLower(rawURL), "github.com/") {
15
+ return "", false
16
+ }
17
+ match := githubLicenseField.FindStringSubmatch(markdown)
18
+ if len(match) != 2 {
19
+ return "", false
20
+ }
21
+ value := match[1]
22
+ evidenceURL := rawURL
23
+ if source := repositoryLicenseURL.FindString(markdown); source != "" {
24
+ evidenceURL = source
25
+ }
26
+ return fmt.Sprintf(
27
+ "## Answer\n\nThe repository lists its license as **%s**.\n\n**Source:** %s\n\n**Evidence:** GitHub repository metadata lists `license: %q`.\n",
28
+ value, evidenceURL, value,
29
+ ), true
30
+ }