webctx 0.2.5 → 0.2.7
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.
- package/README.md +4 -4
- package/internal/app/app.go +34 -11
- package/internal/app/data_audit_test.go +433 -0
- package/internal/app/data_client.go +196 -14
- package/internal/app/data_contract_constraints.go +65 -0
- package/internal/app/data_examples.go +185 -0
- package/internal/app/data_execute.go +231 -79
- package/internal/app/data_inspect.go +21 -40
- package/internal/app/data_pagination.go +126 -0
- package/internal/app/data_replay.go +83 -0
- package/internal/app/data_research.go +178 -6
- package/internal/app/data_tools_test.go +124 -16
- package/internal/app/data_types.go +38 -16
- package/internal/app/research.go +24 -5
- package/internal/app/research_answers.go +30 -0
- package/internal/app/research_copyable.go +48 -0
- package/internal/app/research_copyable_test.go +57 -0
- package/internal/app/site_map_page.go +256 -0
- package/internal/app/tools.go +16 -46
- package/package.json +1 -1
- package/pkg/origo/data_test.go +31 -5
- package/pkg/origo/server.go +106 -28
- package/pkg/origo/server_test.go +56 -4
- package/pkg/retrieval/data.go +3 -0
|
@@ -3,6 +3,7 @@ package app
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"crypto/rand"
|
|
6
|
+
"crypto/sha256"
|
|
6
7
|
"encoding/hex"
|
|
7
8
|
"encoding/json"
|
|
8
9
|
"errors"
|
|
@@ -14,18 +15,27 @@ import (
|
|
|
14
15
|
"regexp"
|
|
15
16
|
"strconv"
|
|
16
17
|
"strings"
|
|
18
|
+
"sync"
|
|
17
19
|
"time"
|
|
20
|
+
|
|
21
|
+
"golang.org/x/sync/singleflight"
|
|
18
22
|
)
|
|
19
23
|
|
|
20
24
|
const dataAPIBase = "https://api.firecrawl.dev/v2"
|
|
21
|
-
|
|
25
|
+
|
|
26
|
+
// The server, not a client-provided tool parameter, controls this preflight ceiling.
|
|
27
|
+
// A production override is configured on the Origo Vercel project.
|
|
28
|
+
const defaultDataCreditBudget = 200
|
|
22
29
|
const maxDataResponseBytes int64 = 12 << 20
|
|
23
30
|
|
|
24
31
|
type DataError struct {
|
|
25
|
-
Code
|
|
26
|
-
Message
|
|
27
|
-
RequiresAction
|
|
28
|
-
Details
|
|
32
|
+
Code string `json:"code"`
|
|
33
|
+
Message string `json:"message"`
|
|
34
|
+
RequiresAction map[string]any `json:"requires_action,omitempty"`
|
|
35
|
+
Details any `json:"details,omitempty"`
|
|
36
|
+
RetryAfterSeconds int `json:"retry_after_seconds,omitempty"`
|
|
37
|
+
RetryAt string `json:"retry_at,omitempty"`
|
|
38
|
+
Retryable bool `json:"retryable,omitempty"`
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
func (e *DataError) Error() string { return e.Code + ": " + e.Message }
|
|
@@ -43,22 +53,108 @@ func dataAPIKey() (string, error) {
|
|
|
43
53
|
|
|
44
54
|
func dataCreditCap() int {
|
|
45
55
|
value, err := strconv.Atoi(strings.TrimSpace(os.Getenv("WEBCTX_ALEXANDRIA_MAX_CREDITS")))
|
|
46
|
-
if err != nil || value < 1 || value >
|
|
56
|
+
if err != nil || value < 1 || value > defaultDataCreditBudget {
|
|
47
57
|
return defaultDataCreditBudget
|
|
48
58
|
}
|
|
49
59
|
return value
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
var safeRequestID = regexp.MustCompile("^[A-Za-z0-9_-]{8,128}$")
|
|
63
|
+
|
|
64
|
+
var freeDataCooldown struct {
|
|
65
|
+
sync.Mutex
|
|
66
|
+
until time.Time
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type dataReadRetryKey struct{}
|
|
70
|
+
|
|
71
|
+
var upstreamDataGate = make(chan struct{}, 3)
|
|
72
|
+
|
|
73
|
+
type catalogueCacheEntry struct {
|
|
74
|
+
payload map[string]any
|
|
75
|
+
response map[string]any
|
|
76
|
+
expires time.Time
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var catalogueCache = struct {
|
|
80
|
+
sync.Mutex
|
|
81
|
+
entries map[string]catalogueCacheEntry
|
|
82
|
+
}{entries: map[string]catalogueCacheEntry{}}
|
|
83
|
+
var catalogueFlight singleflight.Group
|
|
84
|
+
|
|
85
|
+
func cacheableCatalogueKey(options map[string]any) string {
|
|
86
|
+
key, err := dataAPIKey()
|
|
87
|
+
if err != nil || strings.HasPrefix(key, "test") {
|
|
88
|
+
return ""
|
|
89
|
+
}
|
|
90
|
+
snapshot, err := json.Marshal(options)
|
|
91
|
+
if err != nil {
|
|
92
|
+
return ""
|
|
58
93
|
}
|
|
94
|
+
hash := sha256.Sum256(append(append([]byte(key), 0), snapshot...))
|
|
95
|
+
return hex.EncodeToString(hash[:])
|
|
59
96
|
}
|
|
60
97
|
|
|
61
|
-
|
|
98
|
+
// Coordinate upstream read traffic per warm worker. Vercel has multiple
|
|
99
|
+
// workers, so this is a local best-effort throttle, not a global quota.
|
|
100
|
+
func dataReadCooldown(ctx context.Context) error {
|
|
101
|
+
freeDataCooldown.Lock()
|
|
102
|
+
until := freeDataCooldown.until
|
|
103
|
+
freeDataCooldown.Unlock()
|
|
104
|
+
if remaining := time.Until(until); remaining > 0 {
|
|
105
|
+
if remaining < 4*time.Second {
|
|
106
|
+
timer := time.NewTimer(remaining)
|
|
107
|
+
defer timer.Stop()
|
|
108
|
+
select {
|
|
109
|
+
case <-timer.C:
|
|
110
|
+
return nil
|
|
111
|
+
case <-ctx.Done():
|
|
112
|
+
return dataError("request_cancelled", "The provider read was cancelled while throttled.")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return rateLimitError(int(remaining.Seconds())+1, "The provider's request allowance is temporarily exhausted.", "local_cooldown")
|
|
116
|
+
}
|
|
117
|
+
return nil
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func rateLimitError(seconds int, message, source string) *DataError {
|
|
121
|
+
if seconds < 1 {
|
|
122
|
+
seconds = 60
|
|
123
|
+
}
|
|
124
|
+
if seconds > 3600 {
|
|
125
|
+
seconds = 3600
|
|
126
|
+
}
|
|
127
|
+
return &DataError{
|
|
128
|
+
Code: "rate_limited", Message: message, Retryable: true, RetryAfterSeconds: seconds,
|
|
129
|
+
RetryAt: time.Now().Add(time.Duration(seconds) * time.Second).UTC().Format(time.RFC3339),
|
|
130
|
+
Details: map[string]any{"retry_after_source": source, "guidance": "Retry this read after the indicated delay. For paid calls, preserve the original request_id and exact inputs."},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func readRetryDelay(headers http.Header) (int, string) {
|
|
135
|
+
if raw := strings.TrimSpace(headers.Get("Retry-After")); raw != "" {
|
|
136
|
+
if n, e := strconv.Atoi(raw); e == nil && n > 0 {
|
|
137
|
+
return n, "retry-after-header"
|
|
138
|
+
}
|
|
139
|
+
if t, e := http.ParseTime(raw); e == nil {
|
|
140
|
+
seconds := int(time.Until(t).Seconds()) + 1
|
|
141
|
+
if seconds > 0 {
|
|
142
|
+
return seconds, "retry-after-header"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if raw := strings.TrimSpace(headers.Get("RateLimit-Reset")); raw != "" {
|
|
147
|
+
if n, e := strconv.ParseInt(raw, 10, 64); e == nil {
|
|
148
|
+
if n > 1e9 {
|
|
149
|
+
n = int64(time.Until(time.Unix(n, 0)).Seconds()) + 1
|
|
150
|
+
}
|
|
151
|
+
if n > 0 && n < 3601 {
|
|
152
|
+
return int(n), "ratelimit-reset-header"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return 60, "estimated_minute_window"
|
|
157
|
+
}
|
|
62
158
|
|
|
63
159
|
func newRequestID() (string, error) {
|
|
64
160
|
var bytes [16]byte
|
|
@@ -86,7 +182,18 @@ func doDataAPI(ctx context.Context, endpoint string, payload any, requestID stri
|
|
|
86
182
|
if requestID != "" {
|
|
87
183
|
req.Header.Set("x-request-id", requestID)
|
|
88
184
|
}
|
|
185
|
+
if requestID == "" {
|
|
186
|
+
if err := dataReadCooldown(ctx); err != nil {
|
|
187
|
+
return nil, err
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
select {
|
|
191
|
+
case upstreamDataGate <- struct{}{}:
|
|
192
|
+
case <-ctx.Done():
|
|
193
|
+
return nil, dataError("request_cancelled", "Timed out waiting for an upstream request slot.")
|
|
194
|
+
}
|
|
89
195
|
res, err := http.DefaultClient.Do(req)
|
|
196
|
+
<-upstreamDataGate
|
|
90
197
|
if err != nil {
|
|
91
198
|
return nil, &DataError{Code: "network_error", Message: "The provider request did not complete. For a paid call, retry only with the same request_id to prevent duplicate charges."}
|
|
92
199
|
}
|
|
@@ -100,6 +207,10 @@ func doDataAPI(ctx context.Context, endpoint string, payload any, requestID stri
|
|
|
100
207
|
}
|
|
101
208
|
var decoded map[string]any
|
|
102
209
|
if json.Unmarshal(raw, &decoded) != nil {
|
|
210
|
+
if res.StatusCode == http.StatusTooManyRequests {
|
|
211
|
+
seconds, source := readRetryDelay(res.Header)
|
|
212
|
+
return nil, rateLimitError(seconds, "The upstream data provider is rate limiting requests.", source)
|
|
213
|
+
}
|
|
103
214
|
return nil, dataError("invalid_upstream_response", fmt.Sprintf("Provider returned non-JSON HTTP %d.", res.StatusCode))
|
|
104
215
|
}
|
|
105
216
|
if res.StatusCode < 200 || res.StatusCode >= 300 || decoded["success"] == false {
|
|
@@ -117,7 +228,36 @@ func doDataAPI(ctx context.Context, endpoint string, payload any, requestID stri
|
|
|
117
228
|
if len(message) > 800 {
|
|
118
229
|
message = message[:800]
|
|
119
230
|
}
|
|
231
|
+
if res.StatusCode == http.StatusTooManyRequests || strings.EqualFold(code, "rate_limit_exceeded") || strings.EqualFold(code, "rate_limited") {
|
|
232
|
+
seconds, source := readRetryDelay(res.Header)
|
|
233
|
+
if requestID == "" {
|
|
234
|
+
freeDataCooldown.Lock()
|
|
235
|
+
next := time.Now().Add(time.Duration(seconds) * time.Second)
|
|
236
|
+
if next.After(freeDataCooldown.until) {
|
|
237
|
+
freeDataCooldown.until = next
|
|
238
|
+
}
|
|
239
|
+
freeDataCooldown.Unlock()
|
|
240
|
+
}
|
|
241
|
+
// A short retry is safe for free discovery. Never automatically
|
|
242
|
+
// repeat a paid execution: its actual charging state may be unknown.
|
|
243
|
+
attempt, _ := ctx.Value(dataReadRetryKey{}).(int)
|
|
244
|
+
if requestID == "" && seconds <= 2 && attempt == 0 {
|
|
245
|
+
timer := time.NewTimer(time.Duration(seconds) * time.Second)
|
|
246
|
+
defer timer.Stop()
|
|
247
|
+
select {
|
|
248
|
+
case <-timer.C:
|
|
249
|
+
return doDataAPI(context.WithValue(ctx, dataReadRetryKey{}, 1), endpoint, payload, requestID)
|
|
250
|
+
case <-ctx.Done():
|
|
251
|
+
return nil, rateLimitError(seconds, "The provider's allowance has not recovered yet.", source)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return nil, rateLimitError(seconds, "The upstream data provider is rate limiting requests.", source)
|
|
255
|
+
}
|
|
120
256
|
out := &DataError{Code: code, Message: message}
|
|
257
|
+
if strings.EqualFold(code, "duplicate_request") {
|
|
258
|
+
out.Message = "This request_id was already used with different inputs. Reuse the exact original request body to replay it, or omit request_id to start a new query."
|
|
259
|
+
out.Details = map[string]any{"guidance": "Do not retry the conflicting payload with the same request_id.", "recovery": "Restore the exact original calls for replay, or generate a fresh request_id for intentionally new calls."}
|
|
260
|
+
}
|
|
121
261
|
if action, ok := decoded["requiresAction"].(map[string]any); ok {
|
|
122
262
|
if target := stringValue(action["url"]); target != "" {
|
|
123
263
|
if u, e := url.Parse(target); e == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Host != "" {
|
|
@@ -154,6 +294,43 @@ func extractNativeData(response map[string]any) (map[string]any, error) {
|
|
|
154
294
|
}
|
|
155
295
|
|
|
156
296
|
func freeDataBrowse(ctx context.Context, options map[string]any) (map[string]any, map[string]any, error) {
|
|
297
|
+
cacheKey := cacheableCatalogueKey(options)
|
|
298
|
+
if cacheKey != "" {
|
|
299
|
+
catalogueCache.Lock()
|
|
300
|
+
cached, ok := catalogueCache.entries[cacheKey]
|
|
301
|
+
catalogueCache.Unlock()
|
|
302
|
+
if ok && time.Now().Before(cached.expires) {
|
|
303
|
+
return cached.payload, cached.response, nil
|
|
304
|
+
}
|
|
305
|
+
type cacheResult struct {
|
|
306
|
+
payload map[string]any
|
|
307
|
+
response map[string]any
|
|
308
|
+
}
|
|
309
|
+
result, err, _ := catalogueFlight.Do(cacheKey, func() (any, error) {
|
|
310
|
+
data, raw, e := uncachedFreeDataBrowse(ctx, options)
|
|
311
|
+
if e != nil {
|
|
312
|
+
return nil, e
|
|
313
|
+
}
|
|
314
|
+
catalogueCache.Lock()
|
|
315
|
+
if len(catalogueCache.entries) > 256 {
|
|
316
|
+
catalogueCache.entries = map[string]catalogueCacheEntry{}
|
|
317
|
+
}
|
|
318
|
+
catalogueCache.entries[cacheKey] = catalogueCacheEntry{
|
|
319
|
+
payload: data, response: raw, expires: time.Now().Add(2 * time.Minute),
|
|
320
|
+
}
|
|
321
|
+
catalogueCache.Unlock()
|
|
322
|
+
return cacheResult{payload: data, response: raw}, nil
|
|
323
|
+
})
|
|
324
|
+
if err != nil {
|
|
325
|
+
return nil, nil, err
|
|
326
|
+
}
|
|
327
|
+
got := result.(cacheResult)
|
|
328
|
+
return got.payload, got.response, nil
|
|
329
|
+
}
|
|
330
|
+
return uncachedFreeDataBrowse(ctx, options)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
func uncachedFreeDataBrowse(ctx context.Context, options map[string]any) (map[string]any, map[string]any, error) {
|
|
157
334
|
response, err := nativeDataCall(ctx, "firecrawl", "find-tools", options, "")
|
|
158
335
|
if err != nil {
|
|
159
336
|
return nil, nil, err
|
|
@@ -164,7 +341,12 @@ func freeDataBrowse(ctx context.Context, options map[string]any) (map[string]any
|
|
|
164
341
|
}
|
|
165
342
|
details, ok := item["data"].(map[string]any)
|
|
166
343
|
if !ok {
|
|
167
|
-
|
|
344
|
+
if item["data"] == nil && item["error"] == nil {
|
|
345
|
+
// Some Alexandria catalogues signal an unknown ID by returning
|
|
346
|
+
// a successful find-tools call with data=null.
|
|
347
|
+
return map[string]any{"items": []any{}, "total": 0}, response, nil
|
|
348
|
+
}
|
|
349
|
+
return nil, nil, dataError("invalid_upstream_response", "The catalogue returned an unreadable response. Retry discovery before selecting an operation.")
|
|
168
350
|
}
|
|
169
351
|
return details, response, nil
|
|
170
352
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"strings"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
var integerDataInputs = map[string]bool{
|
|
8
|
+
"page": true, "page_number": true, "pageNumber": true,
|
|
9
|
+
"page_size": true, "per_page": true, "offset": true,
|
|
10
|
+
"limit": true, "k": true, "top_k": true, "topK": true,
|
|
11
|
+
"count": true, "size": true, "max_results": true,
|
|
12
|
+
"num_results": true, "max_records": true, "results_per_page": true,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Return normalized, machine-readable hints alongside the untouched native
|
|
16
|
+
// field contract. Do not overwrite the upstream schema's declared types.
|
|
17
|
+
func normalizedDataInputFields(contract map[string]any) []map[string]any {
|
|
18
|
+
fields := make([]map[string]any, 0)
|
|
19
|
+
for _, v := range listField(contract["options"]) {
|
|
20
|
+
item := mapField(v)
|
|
21
|
+
if item == nil {
|
|
22
|
+
continue
|
|
23
|
+
}
|
|
24
|
+
name := stringField(item["name"])
|
|
25
|
+
if name == "" {
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
normalized := map[string]any{"name": name, "type": item["type"]}
|
|
29
|
+
typ := strings.ToLower(stringField(item["type"]))
|
|
30
|
+
description := strings.ToLower(stringField(item["about"]))
|
|
31
|
+
if typ == "number" && (integerDataInputs[name] || strings.Contains(description, "integer")) {
|
|
32
|
+
normalized["type"] = "integer"
|
|
33
|
+
normalized["original_type"] = item["type"]
|
|
34
|
+
}
|
|
35
|
+
for _, key := range []string{"pattern", "required", "min", "max", "default", "oneOf", "nullable", "requires", "dependsOn", "mutuallyExclusiveWith"} {
|
|
36
|
+
if value, ok := item[key]; ok {
|
|
37
|
+
normalized[key] = value
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
fields = append(fields, normalized)
|
|
41
|
+
}
|
|
42
|
+
return fields
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
func normalizedDataOutputFields(contract map[string]any) []map[string]any {
|
|
46
|
+
response := mapField(contract["response"])
|
|
47
|
+
fields := make([]map[string]any, 0)
|
|
48
|
+
for _, v := range listField(response["fields"]) {
|
|
49
|
+
item := mapField(v)
|
|
50
|
+
if item == nil {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
normalized := map[string]any{"name": item["name"], "type": item["type"]}
|
|
54
|
+
about := strings.ToLower(stringField(item["about"]))
|
|
55
|
+
nullable, _ := item["nullable"].(bool)
|
|
56
|
+
if strings.Contains(about, "null") || strings.Contains(about, "missing value") {
|
|
57
|
+
nullable = true
|
|
58
|
+
}
|
|
59
|
+
if nullable {
|
|
60
|
+
normalized["nullable"] = true
|
|
61
|
+
}
|
|
62
|
+
fields = append(fields, normalized)
|
|
63
|
+
}
|
|
64
|
+
return fields
|
|
65
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"regexp"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
var backtickedIdentifier = regexp.MustCompile("`([A-Za-z][A-Za-z0-9_/-]{1,30})`")
|
|
9
|
+
|
|
10
|
+
// Upstream examples can legitimately be absent. Offer a clearly marked,
|
|
11
|
+
// locally validated sample input without claiming it was published by the
|
|
12
|
+
// provider or that it was executed.
|
|
13
|
+
func contractExamples(contract map[string]any) any {
|
|
14
|
+
if published := contract["examples"]; published != nil {
|
|
15
|
+
if len(listField(published)) > 0 {
|
|
16
|
+
return published
|
|
17
|
+
}
|
|
18
|
+
if len(mapField(published)) > 0 {
|
|
19
|
+
return published
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
input, needed := nativeInputTemplate(contract)
|
|
23
|
+
fieldInfo := map[string]map[string]any{}
|
|
24
|
+
for _, v := range listField(contract["options"]) {
|
|
25
|
+
if field := mapField(v); field != nil {
|
|
26
|
+
fieldInfo[stringField(field["name"])] = field
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for _, name := range needed {
|
|
30
|
+
field := fieldInfo[name]
|
|
31
|
+
if value, ok := sampleFieldValue(name, field); ok {
|
|
32
|
+
input[name] = value
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
var chooseOne []any
|
|
36
|
+
for _, v := range listField(contract["requiresOneOf"]) {
|
|
37
|
+
alternatives := listField(v)
|
|
38
|
+
satisfied := false
|
|
39
|
+
for _, candidate := range alternatives {
|
|
40
|
+
name := stringField(candidate)
|
|
41
|
+
if input[name] != nil && input[name] != "" {
|
|
42
|
+
satisfied = true
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if satisfied {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
for _, candidate := range alternatives {
|
|
50
|
+
name := stringField(candidate)
|
|
51
|
+
if value, ok := sampleFieldValue(name, fieldInfo[name]); ok {
|
|
52
|
+
input[name] = value
|
|
53
|
+
satisfied = true
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if !satisfied {
|
|
58
|
+
chooseOne = append(chooseOne, alternatives)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// An illustrative example should not accidentally fetch huge datasets
|
|
62
|
+
// when copied verbatim. Users may adjust the inputs freely.
|
|
63
|
+
for _, name := range []string{"limit", "per_page", "page_size", "max_results", "count"} {
|
|
64
|
+
if n, ok := numberField(input[name]); ok && n > 5 {
|
|
65
|
+
input[name] = 5
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if _, _, err := parseOperationID(stringField(contract["id"])); err != nil {
|
|
69
|
+
return nil
|
|
70
|
+
}
|
|
71
|
+
validation := validateDataInputs(contract, input)
|
|
72
|
+
practical := validation == nil
|
|
73
|
+
if len(input) == 0 && len(listField(contract["options"])) > 0 {
|
|
74
|
+
practical = false
|
|
75
|
+
}
|
|
76
|
+
// A purely structural URL or a placeholder never proves a real record
|
|
77
|
+
// exists. High-credit person-profile lookups must use a real user URL.
|
|
78
|
+
for name, value := range input {
|
|
79
|
+
lower := strings.ToLower(name)
|
|
80
|
+
s := stringField(value)
|
|
81
|
+
if strings.Contains(lower, "linkedin") && strings.Contains(s, "example") {
|
|
82
|
+
practical = false
|
|
83
|
+
}
|
|
84
|
+
if strings.Contains(strings.ToLower(s), "placeholder") ||
|
|
85
|
+
strings.Contains(strings.ToLower(s), "identifier") {
|
|
86
|
+
practical = false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
id := strings.ToLower(stringField(contract["id"]))
|
|
90
|
+
if (strings.HasSuffix(id, "/get") || strings.HasSuffix(id, "/lookup") ||
|
|
91
|
+
strings.HasSuffix(id, "/detail")) && len(input) == 0 {
|
|
92
|
+
practical = false
|
|
93
|
+
}
|
|
94
|
+
example := map[string]any{
|
|
95
|
+
"kind": "generated_from_inspected_schema",
|
|
96
|
+
"note": "Illustrative Origo-generated inputs; not a published provider example or an executed query.",
|
|
97
|
+
"inputs": input,
|
|
98
|
+
"structurally_valid": validation == nil,
|
|
99
|
+
"valid_input_shape": validation == nil && practical,
|
|
100
|
+
"ready_to_execute": practical,
|
|
101
|
+
}
|
|
102
|
+
if validation != nil || !practical {
|
|
103
|
+
example["note"] = "Illustrative input template; fill required fields and check source-specific constraints before execution."
|
|
104
|
+
example["needs_input"] = needed
|
|
105
|
+
if len(chooseOne) > 0 {
|
|
106
|
+
example["needs_one_of"] = chooseOne
|
|
107
|
+
}
|
|
108
|
+
} else {
|
|
109
|
+
example["next"] = map[string]any{"tool": "execute", "arguments": map[string]any{
|
|
110
|
+
"calls": []any{map[string]any{"id": contract["id"], "inputs": input}},
|
|
111
|
+
}}
|
|
112
|
+
}
|
|
113
|
+
return []any{example}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func sampleFieldValue(name string, field map[string]any) (any, bool) {
|
|
117
|
+
if field == nil {
|
|
118
|
+
return nil, false
|
|
119
|
+
}
|
|
120
|
+
if values := listField(field["oneOf"]); len(values) > 0 {
|
|
121
|
+
for _, v := range values {
|
|
122
|
+
if v != nil && v != "" {
|
|
123
|
+
return v, true
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if sample, ok := field["example"]; ok && sample != nil {
|
|
128
|
+
if s := stringField(sample); s != "" &&
|
|
129
|
+
(strings.Contains(strings.ToLower(s), "placeholder") ||
|
|
130
|
+
strings.Contains(strings.ToLower(s), "identifier")) {
|
|
131
|
+
// Example-like placeholders are not executable source IDs.
|
|
132
|
+
} else {
|
|
133
|
+
return sample, true
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if samples := listField(field["examples"]); len(samples) > 0 && samples[0] != nil {
|
|
137
|
+
return samples[0], true
|
|
138
|
+
}
|
|
139
|
+
if v, ok := field["default"]; ok && v != nil {
|
|
140
|
+
return v, true
|
|
141
|
+
}
|
|
142
|
+
info := stringField(field["about"]) + " " + stringField(field["description"])
|
|
143
|
+
lower := strings.ToLower(name + " " + info)
|
|
144
|
+
switch {
|
|
145
|
+
case name == "companyKey":
|
|
146
|
+
return "NASDAQ_AAPL", true
|
|
147
|
+
case name == "repo" || name == "repository":
|
|
148
|
+
return "openai/openai-python", true
|
|
149
|
+
case name == "symbol" || name == "ticker":
|
|
150
|
+
return "AAPL", true
|
|
151
|
+
case name == "from" || name == "base_currency":
|
|
152
|
+
return "USD", true
|
|
153
|
+
case name == "to" || name == "quote_currency":
|
|
154
|
+
return "EUR", true
|
|
155
|
+
case name == "domain":
|
|
156
|
+
return "github.com", true
|
|
157
|
+
case strings.Contains(lower, "linkedin") || strings.Contains(lower, "professional_network_url"):
|
|
158
|
+
return nil, false
|
|
159
|
+
case strings.Contains(name, "series_id") && strings.Contains(info, "CPIAUCSL"):
|
|
160
|
+
return "CPIAUCSL", true
|
|
161
|
+
case strings.Contains(name, "series_id") && strings.Contains(info, "UNRATE"):
|
|
162
|
+
return "UNRATE", true
|
|
163
|
+
case strings.Contains(name, "series_id") && strings.Contains(info, "GDP"):
|
|
164
|
+
return "GDP", true
|
|
165
|
+
case strings.Contains(lower, "semantic_search"):
|
|
166
|
+
return "AI agents", true
|
|
167
|
+
case name == "query" || name == "search" || name == "keyword_search":
|
|
168
|
+
return "example topic", true
|
|
169
|
+
case name == "url" || strings.HasSuffix(name, "_url"):
|
|
170
|
+
return "https://example.com", true
|
|
171
|
+
}
|
|
172
|
+
// Backticked descriptions sometimes contain sample codes or IDs.
|
|
173
|
+
if strings.Contains(lower, "id") || strings.Contains(lower, "code") {
|
|
174
|
+
for _, match := range backtickedIdentifier.FindAllStringSubmatch(info, 10) {
|
|
175
|
+
candidate := match[1]
|
|
176
|
+
if strings.Contains(candidate, "/") || strings.Contains(candidate, "_") {
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if len(candidate) >= 3 && !strings.EqualFold(candidate, "required") {
|
|
180
|
+
return candidate, true
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return nil, false
|
|
185
|
+
}
|