webctx 0.2.4 → 0.2.6
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/CONTRIBUTORS.md +1 -1
- package/README.md +23 -5
- package/internal/app/app.go +133 -0
- package/internal/app/data_client.go +214 -0
- package/internal/app/data_examples.go +112 -0
- package/internal/app/data_execute.go +435 -0
- package/internal/app/data_inspect.go +174 -0
- package/internal/app/data_research.go +340 -0
- package/internal/app/data_tools_test.go +444 -0
- package/internal/app/data_types.go +39 -0
- package/internal/app/research.go +20 -4
- package/internal/app/research_copyable.go +48 -0
- package/internal/app/research_copyable_test.go +57 -0
- package/package.json +2 -2
- package/pkg/origo/data_test.go +134 -0
- package/pkg/origo/server.go +117 -0
- package/pkg/origo/server_test.go +63 -6
- package/pkg/retrieval/data.go +14 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"sort"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func researchView(view, query string) (string, error) {
|
|
9
|
+
switch strings.ToLower(strings.TrimSpace(view)) {
|
|
10
|
+
case "":
|
|
11
|
+
if strings.TrimSpace(query) == "" {
|
|
12
|
+
return "providers", nil
|
|
13
|
+
}
|
|
14
|
+
return "tools", nil
|
|
15
|
+
case "sources", "providers":
|
|
16
|
+
return "providers", nil
|
|
17
|
+
case "groups":
|
|
18
|
+
return "groups", nil
|
|
19
|
+
case "operations", "tools":
|
|
20
|
+
return "tools", nil
|
|
21
|
+
default:
|
|
22
|
+
return "", dataError("invalid_view", "Use view=sources, groups, or operations.")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func expandContract(include []string) ([]string, error) {
|
|
27
|
+
seen := map[string]bool{}
|
|
28
|
+
out := make([]string, 0, len(include))
|
|
29
|
+
for _, value := range include {
|
|
30
|
+
name := ""
|
|
31
|
+
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
32
|
+
case "inputs", "options":
|
|
33
|
+
name = "options"
|
|
34
|
+
case "output", "response":
|
|
35
|
+
name = "response"
|
|
36
|
+
case "examples":
|
|
37
|
+
name = "examples"
|
|
38
|
+
default:
|
|
39
|
+
return nil, dataError("invalid_include", "Supported include values: inputs, output, examples.")
|
|
40
|
+
}
|
|
41
|
+
if !seen[name] {
|
|
42
|
+
out = append(out, name)
|
|
43
|
+
seen[name] = true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out, nil
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func normalizeResearchInput(input ResearchDataInput) (ResearchDataInput, string, []string, error) {
|
|
50
|
+
switch input.Mode {
|
|
51
|
+
case "", "ranked", "catalogue":
|
|
52
|
+
default:
|
|
53
|
+
return input, "", nil, dataError("invalid_mode", "Use mode=ranked or mode=catalogue.")
|
|
54
|
+
}
|
|
55
|
+
view, err := researchView(input.View, input.Query)
|
|
56
|
+
if err != nil {
|
|
57
|
+
return input, "", nil, err
|
|
58
|
+
}
|
|
59
|
+
expand, err := expandContract(input.Include)
|
|
60
|
+
if err != nil {
|
|
61
|
+
return input, "", nil, err
|
|
62
|
+
}
|
|
63
|
+
if input.Limit == 0 {
|
|
64
|
+
input.Limit = 10
|
|
65
|
+
}
|
|
66
|
+
if input.Limit < 1 || input.Limit > 100 {
|
|
67
|
+
return input, "", nil, dataError("invalid_limit", "limit must be between 1 and 100.")
|
|
68
|
+
}
|
|
69
|
+
if input.Offset < 0 || input.Offset > 100000 {
|
|
70
|
+
return input, "", nil, dataError("invalid_offset", "offset must be between 0 and 100000.")
|
|
71
|
+
}
|
|
72
|
+
for _, target := range input.URLs {
|
|
73
|
+
if err := validateSourceURL(target); err != nil {
|
|
74
|
+
return input, "", nil, dataError("invalid_url", "Website filters require public HTTP(S) URLs.")
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return input, view, expand, nil
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func nativeBrowseOptions(input ResearchDataInput, view string, expand []string) map[string]any {
|
|
81
|
+
o := map[string]any{"level": view, "limit": input.Limit, "offset": input.Offset}
|
|
82
|
+
if input.Query != "" {
|
|
83
|
+
o["query"] = input.Query
|
|
84
|
+
}
|
|
85
|
+
if len(input.URLs) > 0 {
|
|
86
|
+
o["urls"] = input.URLs
|
|
87
|
+
}
|
|
88
|
+
if len(input.Sources) > 0 {
|
|
89
|
+
o["providers"] = input.Sources
|
|
90
|
+
}
|
|
91
|
+
if len(input.Categories) > 0 {
|
|
92
|
+
o["categories"] = input.Categories
|
|
93
|
+
}
|
|
94
|
+
if len(input.Groups) > 0 {
|
|
95
|
+
o["groups"] = input.Groups
|
|
96
|
+
}
|
|
97
|
+
if len(input.Operations) > 0 {
|
|
98
|
+
var caps []string
|
|
99
|
+
sourceSet := map[string]bool{}
|
|
100
|
+
for _, source := range input.Sources {
|
|
101
|
+
sourceSet[source] = true
|
|
102
|
+
}
|
|
103
|
+
for _, id := range input.Operations {
|
|
104
|
+
provider, capability, err := parseOperationID(id)
|
|
105
|
+
if err == nil {
|
|
106
|
+
caps = append(caps, capability)
|
|
107
|
+
sourceSet[provider] = true
|
|
108
|
+
} else {
|
|
109
|
+
caps = append(caps, id)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
o["capabilities"] = caps
|
|
113
|
+
if len(input.Sources) == 0 {
|
|
114
|
+
var providers []string
|
|
115
|
+
for provider := range sourceSet {
|
|
116
|
+
providers = append(providers, provider)
|
|
117
|
+
}
|
|
118
|
+
// Sorting keeps responses, cursors and test fixtures stable.
|
|
119
|
+
sortStrings(providers)
|
|
120
|
+
if len(providers) > 0 {
|
|
121
|
+
o["providers"] = providers
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if len(expand) > 0 {
|
|
126
|
+
o["expand"] = expand
|
|
127
|
+
}
|
|
128
|
+
return o
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func sortStrings(values []string) {
|
|
132
|
+
for i := 1; i < len(values); i++ {
|
|
133
|
+
for j := i; j > 0 && values[j] < values[j-1]; j-- {
|
|
134
|
+
values[j], values[j-1] = values[j-1], values[j]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ResearchData supports ranked semantic search and the complete filterable,
|
|
140
|
+
// pageable catalogue. Each result includes the exact next tool/arguments.
|
|
141
|
+
func ResearchData(input ResearchDataInput) (map[string]any, error) {
|
|
142
|
+
input, view, expand, err := normalizeResearchInput(input)
|
|
143
|
+
if err != nil {
|
|
144
|
+
return nil, err
|
|
145
|
+
}
|
|
146
|
+
ctx, cancel := defaultDataContext()
|
|
147
|
+
defer cancel()
|
|
148
|
+
semantic := input.Mode != "catalogue" && input.Query != "" && view == "tools" && input.Offset == 0 &&
|
|
149
|
+
len(input.URLs) == 0 && len(input.Sources) == 0 && len(input.Categories) == 0 &&
|
|
150
|
+
len(input.Groups) == 0 && len(input.Operations) == 0
|
|
151
|
+
var items []any
|
|
152
|
+
var total any
|
|
153
|
+
var next any
|
|
154
|
+
var raw map[string]any
|
|
155
|
+
mode := "catalogue"
|
|
156
|
+
if semantic {
|
|
157
|
+
mode = "ranked_search"
|
|
158
|
+
// Ask for full metadata even when the agent requested concise output:
|
|
159
|
+
// compact upstream matches may omit both price and operation name.
|
|
160
|
+
// We still return compact result cards unless include was requested.
|
|
161
|
+
detail := "full"
|
|
162
|
+
raw, err = doDataAPI(ctx, "/search", map[string]any{
|
|
163
|
+
"query": input.Query, "sources": []string{"alexandria"},
|
|
164
|
+
"limit": input.Limit, "toolDetail": detail,
|
|
165
|
+
}, "")
|
|
166
|
+
if err != nil {
|
|
167
|
+
return nil, err
|
|
168
|
+
}
|
|
169
|
+
data := mapField(raw["data"])
|
|
170
|
+
items = listField(data["tools"])
|
|
171
|
+
// Some upstream ranked matches arrive out of order despite including a
|
|
172
|
+
// similarity score. Present the highest-scoring candidate first, retaining
|
|
173
|
+
// 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
|
+
// Ranked search has no native cursor. Expose the free, complete
|
|
186
|
+
// catalogue with matching semantic query as an optional expansion.
|
|
187
|
+
next = map[string]any{"tool": "research", "arguments": map[string]any{
|
|
188
|
+
"query": input.Query, "view": "operations", "mode": "catalogue", "limit": input.Limit,
|
|
189
|
+
}, "note": "Browse all matching operations with pagination; ranked discovery itself has no cursor."}
|
|
190
|
+
} else {
|
|
191
|
+
options := nativeBrowseOptions(input, view, expand)
|
|
192
|
+
var catalogue map[string]any
|
|
193
|
+
catalogue, raw, err = freeDataBrowse(ctx, options)
|
|
194
|
+
if err != nil {
|
|
195
|
+
return nil, err
|
|
196
|
+
}
|
|
197
|
+
items = listField(catalogue["items"])
|
|
198
|
+
total = catalogue["total"]
|
|
199
|
+
if native := mapField(catalogue["next"]); native != nil {
|
|
200
|
+
next = continuationFromCatalogue(native)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
results := make([]map[string]any, 0, len(items))
|
|
204
|
+
for _, value := range items {
|
|
205
|
+
entry := mapField(value)
|
|
206
|
+
if entry == nil {
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
id := stringField(entry["id"])
|
|
210
|
+
if id == "" && view == "tools" {
|
|
211
|
+
if provider := stringField(entry["provider"]); provider != "" {
|
|
212
|
+
id = provider + "/" + stringField(entry["capability"])
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
name := stringField(entry["name"])
|
|
216
|
+
if name == "" {
|
|
217
|
+
segments := strings.Split(id, "/")
|
|
218
|
+
name = strings.ReplaceAll(strings.ReplaceAll(segments[len(segments)-1], "_", " "), "-", " ")
|
|
219
|
+
}
|
|
220
|
+
result := map[string]any{"id": id, "name": name, "description": entry["description"]}
|
|
221
|
+
if provider := stringField(entry["provider"]); provider != "" {
|
|
222
|
+
result["source_id"] = provider
|
|
223
|
+
}
|
|
224
|
+
if cost, ok := entry["creditsCost"]; ok {
|
|
225
|
+
result["credits_per_call"] = cost
|
|
226
|
+
}
|
|
227
|
+
if cost, ok := entry["perRecord"]; ok {
|
|
228
|
+
result["per_record"] = cost
|
|
229
|
+
}
|
|
230
|
+
if count, ok := entry["toolCount"]; ok {
|
|
231
|
+
result["operation_count"] = count
|
|
232
|
+
}
|
|
233
|
+
if rank, ok := entry["similarity"]; ok {
|
|
234
|
+
result["relevance"] = rank
|
|
235
|
+
}
|
|
236
|
+
if len(expand) > 0 {
|
|
237
|
+
if selected := entry["options"]; selected != nil && containsString(expand, "options") {
|
|
238
|
+
result["inputs"] = selected
|
|
239
|
+
}
|
|
240
|
+
if selected := entry["response"]; selected != nil && containsString(expand, "response") {
|
|
241
|
+
result["output"] = selected
|
|
242
|
+
}
|
|
243
|
+
if selected := entry["examples"]; selected != nil && containsString(expand, "examples") {
|
|
244
|
+
result["examples"] = selected
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if continuation := mapField(entry["next"]); continuation != nil {
|
|
248
|
+
result["next"] = continuationFromCatalogue(continuation)
|
|
249
|
+
} else if id != "" {
|
|
250
|
+
result["next"] = map[string]any{"tool": "inspect", "arguments": map[string]any{"id": id}}
|
|
251
|
+
}
|
|
252
|
+
results = append(results, result)
|
|
253
|
+
}
|
|
254
|
+
output := map[string]any{
|
|
255
|
+
"mode": mode, "view": niceDataView(view), "results": results,
|
|
256
|
+
"count": len(results), "cost_credits": 0,
|
|
257
|
+
"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
|
+
}
|
|
259
|
+
if total != nil {
|
|
260
|
+
output["total"] = total
|
|
261
|
+
}
|
|
262
|
+
if next != nil {
|
|
263
|
+
output["next"] = next
|
|
264
|
+
}
|
|
265
|
+
if input.Raw {
|
|
266
|
+
output["raw"] = raw
|
|
267
|
+
}
|
|
268
|
+
return output, nil
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func niceDataView(view string) string {
|
|
272
|
+
switch view {
|
|
273
|
+
case "providers":
|
|
274
|
+
return "sources"
|
|
275
|
+
case "tools":
|
|
276
|
+
return "operations"
|
|
277
|
+
default:
|
|
278
|
+
return view
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
func containsString(items []string, needle string) bool {
|
|
283
|
+
for _, item := range items {
|
|
284
|
+
if item == needle {
|
|
285
|
+
return true
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
func continuationFromCatalogue(native map[string]any) map[string]any {
|
|
292
|
+
options := mapField(native["options"])
|
|
293
|
+
if options == nil {
|
|
294
|
+
return map[string]any{"native_request": native}
|
|
295
|
+
}
|
|
296
|
+
args := map[string]any{"mode": "catalogue"}
|
|
297
|
+
for _, field := range []string{"query", "urls", "providers", "categories", "groups", "limit", "offset"} {
|
|
298
|
+
if value, ok := options[field]; ok {
|
|
299
|
+
name := field
|
|
300
|
+
if field == "providers" {
|
|
301
|
+
name = "sources"
|
|
302
|
+
}
|
|
303
|
+
args[name] = value
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if level := stringField(options["level"]); level != "" {
|
|
307
|
+
args["view"] = niceDataView(level)
|
|
308
|
+
}
|
|
309
|
+
if caps := listField(options["capabilities"]); len(caps) > 0 {
|
|
310
|
+
var ids []string
|
|
311
|
+
sourceNames := listField(options["providers"])
|
|
312
|
+
for _, capability := range caps {
|
|
313
|
+
cap := stringField(capability)
|
|
314
|
+
if cap == "" {
|
|
315
|
+
continue
|
|
316
|
+
}
|
|
317
|
+
if len(sourceNames) == 1 {
|
|
318
|
+
ids = append(ids, stringField(sourceNames[0])+"/"+cap)
|
|
319
|
+
} else {
|
|
320
|
+
ids = append(ids, cap)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
args["operations"] = ids
|
|
324
|
+
}
|
|
325
|
+
if exp := listField(options["expand"]); len(exp) > 0 {
|
|
326
|
+
var include []string
|
|
327
|
+
for _, value := range exp {
|
|
328
|
+
switch stringField(value) {
|
|
329
|
+
case "options":
|
|
330
|
+
include = append(include, "inputs")
|
|
331
|
+
case "response":
|
|
332
|
+
include = append(include, "output")
|
|
333
|
+
case "examples":
|
|
334
|
+
include = append(include, "examples")
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
args["include"] = include
|
|
338
|
+
}
|
|
339
|
+
return map[string]any{"tool": "research", "arguments": args, "native_request": native}
|
|
340
|
+
}
|