research-assistant 0.1.0
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/LICENSE +21 -0
- package/README-ZH.md +115 -0
- package/README.md +124 -0
- package/npm/bin/research-assistant.js +70 -0
- package/npm/scripts/postinstall.js +112 -0
- package/package.json +38 -0
- package/pyproject.toml +84 -0
- package/src/research_assistant/__init__.py +7 -0
- package/src/research_assistant/__main__.py +6 -0
- package/src/research_assistant/assets/researcher_persona.md +94 -0
- package/src/research_assistant/assets/skills/research-assistant/SKILL.md +248 -0
- package/src/research_assistant/cli.py +155 -0
- package/src/research_assistant/commands/__init__.py +7 -0
- package/src/research_assistant/commands/ask.py +54 -0
- package/src/research_assistant/commands/doctor.py +201 -0
- package/src/research_assistant/commands/fetch.py +99 -0
- package/src/research_assistant/commands/locate.py +71 -0
- package/src/research_assistant/commands/search.py +161 -0
- package/src/research_assistant/commands/setup.py +249 -0
- package/src/research_assistant/commands/skills.py +60 -0
- package/src/research_assistant/config.py +258 -0
- package/src/research_assistant/errors.py +106 -0
- package/src/research_assistant/fetch/__init__.py +20 -0
- package/src/research_assistant/fetch/browser.py +613 -0
- package/src/research_assistant/fetch/cfbypass.py +318 -0
- package/src/research_assistant/fetch/html2md.py +44 -0
- package/src/research_assistant/fetch/normal.py +69 -0
- package/src/research_assistant/fetch/search_engine.py +324 -0
- package/src/research_assistant/fetch/snapshot.py +50 -0
- package/src/research_assistant/http.py +118 -0
- package/src/research_assistant/installer.py +270 -0
- package/src/research_assistant/locate/__init__.py +5 -0
- package/src/research_assistant/locate/engine.py +289 -0
- package/src/research_assistant/loginstate/__init__.py +10 -0
- package/src/research_assistant/loginstate/daemon.py +145 -0
- package/src/research_assistant/loginstate/daemon_cli.py +21 -0
- package/src/research_assistant/loginstate/daemonctl.py +116 -0
- package/src/research_assistant/loginstate/extension/background.js +167 -0
- package/src/research_assistant/loginstate/extension/manifest.json +15 -0
- package/src/research_assistant/loginstate/extension/popup.html +31 -0
- package/src/research_assistant/loginstate/extension/popup.js +33 -0
- package/src/research_assistant/output.py +140 -0
- package/src/research_assistant/providers/__init__.py +10 -0
- package/src/research_assistant/providers/base.py +93 -0
- package/src/research_assistant/providers/browser.py +91 -0
- package/src/research_assistant/providers/context7.py +171 -0
- package/src/research_assistant/providers/exa.py +353 -0
- package/src/research_assistant/providers/firecrawl.py +644 -0
- package/src/research_assistant/providers/openai_compat.py +293 -0
- package/src/research_assistant/providers/registry.py +62 -0
- package/src/research_assistant/providers/tavily.py +346 -0
- package/src/research_assistant/proxy.py +58 -0
- package/src/research_assistant/targets.py +66 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Exa provider(自写 HTTP,ADR-0010)。
|
|
2
|
+
|
|
3
|
+
API 端点(webReader 查证 docs.exa.ai/reference + /sdks/python-sdk-specification,2026-07-26):
|
|
4
|
+
鉴权:所有端点统一用 HTTP header `x-api-key: <key>`(或 Authorization: Bearer)。
|
|
5
|
+
POST /search 同步,智能搜索(auto 默认融合 neural + keyword)。
|
|
6
|
+
body: query / numResults / type(keyword|neural|fast|auto) / category /
|
|
7
|
+
includeDomains[] / excludeDomains[] /
|
|
8
|
+
startPublishedDate / endPublishedDate /
|
|
9
|
+
startCrawlDate / endCrawlDate / includeText[] / excludeText[] /
|
|
10
|
+
userLocation / context / moderation /
|
|
11
|
+
contents{text,highlights,livecrawl,subpageTarget,subpages}
|
|
12
|
+
resp: {requestId, resolvedSearchType, results[]{title,url,id,score,text,
|
|
13
|
+
highlights,publishedDate,author}, costDollars}
|
|
14
|
+
POST /findSimilar 同步,按 URL 找语义相似页。
|
|
15
|
+
body: {url, numResults, includeDomains[], excludeDomains[], contents{...}}
|
|
16
|
+
resp: {results[]{url,title,id,score,publishedDate}}
|
|
17
|
+
POST /contents 同步,按 id/URL 取正文/摘要/元数据。
|
|
18
|
+
body: {ids[], text, highlights, summary, livecrawl}
|
|
19
|
+
resp: {contents[]{id,url,title,text,highlights,summary}, statuses[]}
|
|
20
|
+
POST /answer 同步(文档支持 SSE stream=True,本实现只走非流式)。
|
|
21
|
+
body: {query, text?, stream?}
|
|
22
|
+
resp: {answer, citations[]{id,url,title,author,publishedDate,text?}, costDollars}
|
|
23
|
+
异步 Research(提交 + 轮询;SDK 默认 poll_interval=2s, max_wait=300s):
|
|
24
|
+
POST /research/v1 body: {instructions, model?(exa-research|exa-research-pro),
|
|
25
|
+
output:{schema?}|{inferSchema?}}
|
|
26
|
+
resp: {id}
|
|
27
|
+
GET /research/v1/{id} resp: {id, status(pending|running|completed|failed|canceled),
|
|
28
|
+
instructions, model, outputSchema?, data?, citations?}
|
|
29
|
+
GET /research/v1 列表(limit/cursor 游标分页)。
|
|
30
|
+
|
|
31
|
+
未验证项(无 key,未 runtime 实测):
|
|
32
|
+
- Research 路径 v0/v1 文档内部冲突:sidebar OpenAPI 标注三者均为 v1(本实现采纳),
|
|
33
|
+
create-a-task 页 curl 示 /research/v0/tasks,get-a-task 页 curl 示 /research/v1/{id}。
|
|
34
|
+
若 POST /research/v1 返回 404,可尝试改 path 为 research/v0/tasks。
|
|
35
|
+
- Research create 响应字段:REST 文档示例 {id},get 轮询响应字段用 researchId 或 id
|
|
36
|
+
不一致;本实现读取时两者兼容(优先 id,回退 researchId)。
|
|
37
|
+
|
|
38
|
+
封装决策(CLI 表面取舍,非 API 限制):
|
|
39
|
+
- /answer 不暴露 stream=True:SSE 需独立流式输出路径,MVP 走非流式一次返回。
|
|
40
|
+
- /search 不暴露 livecrawl/subpageTarget/subpages(contents 子选项)、context、moderation、
|
|
41
|
+
userLocation、crawlDate:低频或仅在 --text 取正文时相关,按需再扩。
|
|
42
|
+
- /research 不实现 GET /research/v1 列表:CLI 单用户场景用不上批量任务监控。
|
|
43
|
+
|
|
44
|
+
对齐 api-contract.md §2.1/§3 的 exa 命令与响应。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
from __future__ import annotations
|
|
48
|
+
|
|
49
|
+
import asyncio
|
|
50
|
+
import json
|
|
51
|
+
from typing import Any
|
|
52
|
+
|
|
53
|
+
from ..config import ProviderConfig
|
|
54
|
+
from ..errors import ArgsError, ProviderError
|
|
55
|
+
from .. import http as http_mod
|
|
56
|
+
from ..proxy import resolve_proxy
|
|
57
|
+
from .base import ArgSpec, Capability, Provider
|
|
58
|
+
from .registry import register
|
|
59
|
+
|
|
60
|
+
DEFAULT_BASE_URL = "https://api.exa.ai"
|
|
61
|
+
|
|
62
|
+
# Research 轮询终态(REST 文档枚举 5 态,canceled/cancelled 两种拼写都收)
|
|
63
|
+
RESEARCH_TERMINAL_STATES = ("completed", "failed", "cancelled", "canceled")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _split_csv(values: list[str] | None) -> list[str]:
|
|
67
|
+
out: list[str] = []
|
|
68
|
+
for v in values or []:
|
|
69
|
+
for part in str(v).split(","):
|
|
70
|
+
part = part.strip()
|
|
71
|
+
if part:
|
|
72
|
+
out.append(part)
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@register
|
|
77
|
+
class ExaProvider(Provider):
|
|
78
|
+
type = "exa"
|
|
79
|
+
command = "exa"
|
|
80
|
+
aliases = ["x"]
|
|
81
|
+
help = "Exa search / similar / contents / answer / research."
|
|
82
|
+
|
|
83
|
+
def capabilities(self) -> list[Capability]:
|
|
84
|
+
return [
|
|
85
|
+
Capability(
|
|
86
|
+
name="search",
|
|
87
|
+
help="Search the web with Exa (auto/keyword/neural/fast).",
|
|
88
|
+
args=[
|
|
89
|
+
ArgSpec(["query"], kind="positional", help="Search query."),
|
|
90
|
+
ArgSpec(["--num-results"], type=int, default=10, help="Number of results (default 10)."),
|
|
91
|
+
ArgSpec(
|
|
92
|
+
["--type"],
|
|
93
|
+
choices=["auto", "keyword", "neural", "fast"],
|
|
94
|
+
default="auto",
|
|
95
|
+
help="Search type (default auto).",
|
|
96
|
+
),
|
|
97
|
+
ArgSpec(["--include-domains"], nargs="+", metavar="DOMAIN", help="Restrict to domains."),
|
|
98
|
+
ArgSpec(["--exclude-domains"], nargs="+", metavar="DOMAIN", help="Domains to drop from results."),
|
|
99
|
+
ArgSpec(
|
|
100
|
+
["--category"],
|
|
101
|
+
help='Data category (e.g. "company", "research paper", "news", "pdf", "github", "tweet", "personal site", "linkedin profile", "financial report").',
|
|
102
|
+
),
|
|
103
|
+
ArgSpec(["--start-date"], metavar="YYYY-MM-DD", help="Results published after this date."),
|
|
104
|
+
ArgSpec(["--end-date"], metavar="YYYY-MM-DD", help="Results published before this date."),
|
|
105
|
+
ArgSpec(
|
|
106
|
+
["--include-text"],
|
|
107
|
+
nargs="+",
|
|
108
|
+
metavar="TEXT",
|
|
109
|
+
help="Strings that must appear in result text (API currently allows 1, up to 5 words).",
|
|
110
|
+
),
|
|
111
|
+
ArgSpec(
|
|
112
|
+
["--exclude-text"],
|
|
113
|
+
nargs="+",
|
|
114
|
+
metavar="TEXT",
|
|
115
|
+
help="Strings that must not appear in result text (first 1000 words checked).",
|
|
116
|
+
),
|
|
117
|
+
ArgSpec(["--text"], action="store_true", help="Include page text in results."),
|
|
118
|
+
ArgSpec(["--highlights"], action="store_true", help="Include highlights in results."),
|
|
119
|
+
],
|
|
120
|
+
handler=self.search,
|
|
121
|
+
),
|
|
122
|
+
Capability(
|
|
123
|
+
name="similar",
|
|
124
|
+
help="Find pages similar to a URL.",
|
|
125
|
+
args=[
|
|
126
|
+
ArgSpec(["url"], kind="positional", help="Source URL."),
|
|
127
|
+
ArgSpec(["--num-results"], type=int, default=10, help="Number of results."),
|
|
128
|
+
],
|
|
129
|
+
handler=self.similar,
|
|
130
|
+
),
|
|
131
|
+
Capability(
|
|
132
|
+
name="contents",
|
|
133
|
+
help="Fetch contents for Exa IDs (URLs).",
|
|
134
|
+
args=[
|
|
135
|
+
ArgSpec(["ids"], kind="positional", nargs="+", metavar="ID", help="Exa IDs / URLs (space or comma separated)."),
|
|
136
|
+
ArgSpec(["--text"], action="store_true", default=True, help="Include text (default on)."),
|
|
137
|
+
ArgSpec(["--highlights"], action="store_true", help="Include highlights."),
|
|
138
|
+
],
|
|
139
|
+
handler=self.contents,
|
|
140
|
+
),
|
|
141
|
+
Capability(
|
|
142
|
+
name="answer",
|
|
143
|
+
help="LLM answer to a question, grounded in Exa search results.",
|
|
144
|
+
args=[
|
|
145
|
+
ArgSpec(["query"], kind="positional", help="Question to answer."),
|
|
146
|
+
ArgSpec(["--text"], action="store_true", help="Include full citation text in results."),
|
|
147
|
+
],
|
|
148
|
+
handler=self.answer,
|
|
149
|
+
),
|
|
150
|
+
Capability(
|
|
151
|
+
name="research",
|
|
152
|
+
help="Async in-depth research; blocks until done or --poll-timeout (default 300s).",
|
|
153
|
+
args=[
|
|
154
|
+
ArgSpec(["instructions"], kind="positional", help="Natural-language research instructions."),
|
|
155
|
+
ArgSpec(
|
|
156
|
+
["--model"],
|
|
157
|
+
default="exa-research",
|
|
158
|
+
help='Research model: "exa-research" (default, adapts to task) or "exa-research-pro" (hardest tasks).',
|
|
159
|
+
),
|
|
160
|
+
ArgSpec(["--output-schema"], metavar="JSON", help="JSON Schema string for structured output."),
|
|
161
|
+
ArgSpec(["--infer-schema"], action="store_true", help="Let Exa infer the output schema via LLM."),
|
|
162
|
+
ArgSpec(["--poll-timeout"], type=int, default=300, help="Seconds to wait for completion (default 300)."),
|
|
163
|
+
ArgSpec(["--async-submit"], action="store_true", help="Submit only; return the task id without polling."),
|
|
164
|
+
],
|
|
165
|
+
handler=self.research,
|
|
166
|
+
),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
def _cfg(self) -> ProviderConfig:
|
|
170
|
+
cfg = self.config.require_provider("exa")
|
|
171
|
+
if not cfg.api_key:
|
|
172
|
+
raise ArgsError("exa: 缺少 api_key", provider="exa")
|
|
173
|
+
if not cfg.base_url:
|
|
174
|
+
cfg.base_url = DEFAULT_BASE_URL
|
|
175
|
+
return cfg
|
|
176
|
+
|
|
177
|
+
async def search(self, args: Any) -> dict[str, Any]:
|
|
178
|
+
cfg = self._cfg()
|
|
179
|
+
body: dict[str, Any] = {
|
|
180
|
+
"query": args.query,
|
|
181
|
+
"numResults": args.num_results,
|
|
182
|
+
"type": args.type,
|
|
183
|
+
"contents": {"text": bool(args.text), "highlights": bool(args.highlights)},
|
|
184
|
+
}
|
|
185
|
+
if args.include_domains:
|
|
186
|
+
body["includeDomains"] = _split_csv(args.include_domains)
|
|
187
|
+
if args.exclude_domains:
|
|
188
|
+
body["excludeDomains"] = _split_csv(args.exclude_domains)
|
|
189
|
+
if args.category:
|
|
190
|
+
body["category"] = args.category
|
|
191
|
+
if args.start_date:
|
|
192
|
+
body["startPublishedDate"] = args.start_date
|
|
193
|
+
if args.end_date:
|
|
194
|
+
body["endPublishedDate"] = args.end_date
|
|
195
|
+
if args.include_text:
|
|
196
|
+
body["includeText"] = _split_csv(args.include_text)
|
|
197
|
+
if args.exclude_text:
|
|
198
|
+
body["excludeText"] = _split_csv(args.exclude_text)
|
|
199
|
+
|
|
200
|
+
async with http_mod.make_client(
|
|
201
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
202
|
+
timeout=cfg.timeout,
|
|
203
|
+
base_url=cfg.base_url,
|
|
204
|
+
headers={"x-api-key": cfg.api_key},
|
|
205
|
+
) as client:
|
|
206
|
+
data = await http_mod.request_json(client, "POST", "search", provider="exa", json_body=body)
|
|
207
|
+
|
|
208
|
+
results = []
|
|
209
|
+
for r in data.get("results", []) or []:
|
|
210
|
+
item: dict[str, Any] = {"url": r.get("url"), "title": r.get("title")}
|
|
211
|
+
if r.get("id"):
|
|
212
|
+
item["id"] = r.get("id")
|
|
213
|
+
if args.text and r.get("text"):
|
|
214
|
+
item["text"] = r.get("text")
|
|
215
|
+
if args.highlights and r.get("highlights"):
|
|
216
|
+
item["highlights"] = r.get("highlights")
|
|
217
|
+
if r.get("score") is not None:
|
|
218
|
+
item["score"] = r.get("score")
|
|
219
|
+
if r.get("publishedDate"):
|
|
220
|
+
item["publishedDate"] = r.get("publishedDate")
|
|
221
|
+
results.append(item)
|
|
222
|
+
return {"results": results}
|
|
223
|
+
|
|
224
|
+
async def similar(self, args: Any) -> dict[str, Any]:
|
|
225
|
+
cfg = self._cfg()
|
|
226
|
+
body = {"url": args.url, "numResults": args.num_results}
|
|
227
|
+
async with http_mod.make_client(
|
|
228
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
229
|
+
timeout=cfg.timeout,
|
|
230
|
+
base_url=cfg.base_url,
|
|
231
|
+
headers={"x-api-key": cfg.api_key},
|
|
232
|
+
) as client:
|
|
233
|
+
data = await http_mod.request_json(client, "POST", "findSimilar", provider="exa", json_body=body)
|
|
234
|
+
results = [{"url": r.get("url"), "title": r.get("title")} for r in data.get("results", []) or []]
|
|
235
|
+
return {"results": results}
|
|
236
|
+
|
|
237
|
+
async def contents(self, args: Any) -> dict[str, Any]:
|
|
238
|
+
cfg = self._cfg()
|
|
239
|
+
ids = _split_csv(args.ids)
|
|
240
|
+
if not ids:
|
|
241
|
+
raise ArgsError("exa contents: 至少提供一个 id", provider="exa")
|
|
242
|
+
body: dict[str, Any] = {"ids": ids, "text": bool(args.text)}
|
|
243
|
+
if args.highlights:
|
|
244
|
+
body["highlights"] = True
|
|
245
|
+
async with http_mod.make_client(
|
|
246
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
247
|
+
timeout=cfg.timeout,
|
|
248
|
+
base_url=cfg.base_url,
|
|
249
|
+
headers={"x-api-key": cfg.api_key},
|
|
250
|
+
) as client:
|
|
251
|
+
data = await http_mod.request_json(client, "POST", "contents", provider="exa", json_body=body)
|
|
252
|
+
contents = []
|
|
253
|
+
for c in data.get("contents", []) or []:
|
|
254
|
+
item: dict[str, Any] = {"id": c.get("id"), "url": c.get("url"), "text": c.get("text")}
|
|
255
|
+
if c.get("highlights"):
|
|
256
|
+
item["highlights"] = c.get("highlights")
|
|
257
|
+
contents.append(item)
|
|
258
|
+
return {"contents": contents}
|
|
259
|
+
|
|
260
|
+
async def answer(self, args: Any) -> dict[str, Any]:
|
|
261
|
+
cfg = self._cfg()
|
|
262
|
+
# 非流式:不传 stream(默认 false),一次拿到完整 answer + citations。
|
|
263
|
+
body: dict[str, Any] = {"query": args.query}
|
|
264
|
+
if args.text:
|
|
265
|
+
body["text"] = True
|
|
266
|
+
async with http_mod.make_client(
|
|
267
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
268
|
+
timeout=cfg.timeout,
|
|
269
|
+
base_url=cfg.base_url,
|
|
270
|
+
headers={"x-api-key": cfg.api_key},
|
|
271
|
+
) as client:
|
|
272
|
+
data = await http_mod.request_json(client, "POST", "answer", provider="exa", json_body=body)
|
|
273
|
+
citations = []
|
|
274
|
+
for c in data.get("citations", []) or []:
|
|
275
|
+
item: dict[str, Any] = {"id": c.get("id"), "url": c.get("url"), "title": c.get("title")}
|
|
276
|
+
if c.get("author"):
|
|
277
|
+
item["author"] = c.get("author")
|
|
278
|
+
if c.get("publishedDate"):
|
|
279
|
+
item["publishedDate"] = c.get("publishedDate")
|
|
280
|
+
if args.text and c.get("text"):
|
|
281
|
+
item["text"] = c.get("text")
|
|
282
|
+
citations.append(item)
|
|
283
|
+
return {"answer": data.get("answer"), "citations": citations}
|
|
284
|
+
|
|
285
|
+
async def _poll_research(self, client: Any, task_id: str, *, poll_timeout: int) -> dict[str, Any]:
|
|
286
|
+
"""轮询 research 任务直到终态(completed/failed/cancelled)或超时。"""
|
|
287
|
+
loop = asyncio.get_event_loop()
|
|
288
|
+
deadline = loop.time() + poll_timeout
|
|
289
|
+
while True:
|
|
290
|
+
data = await http_mod.request_json(
|
|
291
|
+
client, "GET", f"research/v1/{task_id}", provider="exa"
|
|
292
|
+
)
|
|
293
|
+
status = data.get("status")
|
|
294
|
+
if status in RESEARCH_TERMINAL_STATES:
|
|
295
|
+
return data
|
|
296
|
+
if loop.time() >= deadline:
|
|
297
|
+
data = dict(data)
|
|
298
|
+
data["status"] = "timeout"
|
|
299
|
+
return data
|
|
300
|
+
await asyncio.sleep(2)
|
|
301
|
+
|
|
302
|
+
async def research(self, args: Any) -> dict[str, Any]:
|
|
303
|
+
cfg = self._cfg()
|
|
304
|
+
body: dict[str, Any] = {"instructions": args.instructions, "model": args.model}
|
|
305
|
+
# output 二选一:--output-schema 优先;--infer-schema 次之;都不给则不传(默认 markdown 报告)。
|
|
306
|
+
if args.output_schema:
|
|
307
|
+
try:
|
|
308
|
+
schema = json.loads(args.output_schema)
|
|
309
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
310
|
+
raise ArgsError(
|
|
311
|
+
f"exa research: --output-schema 不是合法 JSON ({e})",
|
|
312
|
+
provider="exa",
|
|
313
|
+
) from e
|
|
314
|
+
body["output"] = {"schema": schema}
|
|
315
|
+
elif args.infer_schema:
|
|
316
|
+
body["output"] = {"inferSchema": True}
|
|
317
|
+
|
|
318
|
+
async with http_mod.make_client(
|
|
319
|
+
proxy_url=resolve_proxy(self.config.proxy.url),
|
|
320
|
+
timeout=cfg.timeout,
|
|
321
|
+
base_url=cfg.base_url,
|
|
322
|
+
headers={"x-api-key": cfg.api_key},
|
|
323
|
+
) as client:
|
|
324
|
+
submit = await http_mod.request_json(
|
|
325
|
+
client, "POST", "research/v1", provider="exa", json_body=body
|
|
326
|
+
)
|
|
327
|
+
task_id = submit.get("id") or submit.get("researchId")
|
|
328
|
+
if not task_id:
|
|
329
|
+
raise ProviderError(
|
|
330
|
+
"exa research: 提交响应缺 id",
|
|
331
|
+
provider="exa", details=submit,
|
|
332
|
+
)
|
|
333
|
+
if args.async_submit:
|
|
334
|
+
return {"id": task_id, "status": "submitted"}
|
|
335
|
+
data = await self._poll_research(client, task_id, poll_timeout=args.poll_timeout)
|
|
336
|
+
|
|
337
|
+
# 兼容 id / researchId 两种字段名(REST 文档示例不一致)
|
|
338
|
+
out: dict[str, Any] = {
|
|
339
|
+
"id": data.get("id") or data.get("researchId") or task_id,
|
|
340
|
+
"status": data.get("status"),
|
|
341
|
+
}
|
|
342
|
+
if data.get("instructions"):
|
|
343
|
+
out["instructions"] = data.get("instructions")
|
|
344
|
+
if data.get("data") is not None:
|
|
345
|
+
out["data"] = data.get("data")
|
|
346
|
+
if data.get("citations") is not None:
|
|
347
|
+
out["citations"] = data.get("citations")
|
|
348
|
+
if data.get("status") != "completed":
|
|
349
|
+
out["note"] = (
|
|
350
|
+
"task not completed; partial or timeout. "
|
|
351
|
+
"Resubmit with --async-submit and poll research/v1/<id>."
|
|
352
|
+
)
|
|
353
|
+
return out
|