raysurfer 0.4.1__py3-none-any.whl
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.
- raysurfer/__init__.py +101 -0
- raysurfer/client.py +773 -0
- raysurfer/exceptions.py +21 -0
- raysurfer/sdk_client.py +552 -0
- raysurfer/sdk_types.py +30 -0
- raysurfer/types.py +198 -0
- raysurfer-0.4.1.dist-info/METADATA +157 -0
- raysurfer-0.4.1.dist-info/RECORD +9 -0
- raysurfer-0.4.1.dist-info/WHEEL +4 -0
raysurfer/client.py
ADDED
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
"""RaySurfer SDK client"""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from raysurfer.exceptions import APIError, AuthenticationError
|
|
8
|
+
from raysurfer.sdk_types import CodeFile, GetCodeFilesResponse
|
|
9
|
+
from raysurfer.types import (
|
|
10
|
+
AgentReview,
|
|
11
|
+
AgentVerdict,
|
|
12
|
+
AlternativeCandidate,
|
|
13
|
+
BestMatch,
|
|
14
|
+
CodeBlock,
|
|
15
|
+
CodeBlockMatch,
|
|
16
|
+
ExecutionIO,
|
|
17
|
+
ExecutionState,
|
|
18
|
+
FewShotExample,
|
|
19
|
+
FileWritten,
|
|
20
|
+
RetrieveBestResponse,
|
|
21
|
+
RetrieveCodeBlockResponse,
|
|
22
|
+
SnipsDesired,
|
|
23
|
+
StoreCodeBlockResponse,
|
|
24
|
+
StoreExecutionResponse,
|
|
25
|
+
SubmitExecutionResultResponse,
|
|
26
|
+
TaskPattern,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
DEFAULT_BASE_URL = "https://web-production-3d338.up.railway.app"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AsyncRaySurfer:
|
|
33
|
+
"""Async client for RaySurfer API"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
api_key: str | None = None,
|
|
38
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
39
|
+
timeout: float = 60.0,
|
|
40
|
+
organization_id: str | None = None,
|
|
41
|
+
workspace_id: str | None = None,
|
|
42
|
+
public_snips: bool = False,
|
|
43
|
+
snips_desired: SnipsDesired | str | None = None,
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the RaySurfer async client.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
api_key: RaySurfer API key (or set RAYSURFER_API_KEY env var)
|
|
50
|
+
base_url: API base URL
|
|
51
|
+
timeout: Request timeout in seconds
|
|
52
|
+
organization_id: Optional organization ID for dedicated namespace (team/enterprise)
|
|
53
|
+
workspace_id: Optional workspace ID for client-specific namespace (enterprise only)
|
|
54
|
+
public_snips: Whether to include public/shared snippets in retrieval (default: False)
|
|
55
|
+
snips_desired: Scope of private snippets - "company" (Team/Enterprise) or "client" (Enterprise only)
|
|
56
|
+
"""
|
|
57
|
+
self.api_key = api_key
|
|
58
|
+
self.base_url = base_url.rstrip("/")
|
|
59
|
+
self.timeout = timeout
|
|
60
|
+
self.organization_id = organization_id
|
|
61
|
+
self.workspace_id = workspace_id
|
|
62
|
+
self.public_snips = public_snips
|
|
63
|
+
# Convert string to SnipsDesired if needed
|
|
64
|
+
if isinstance(snips_desired, str):
|
|
65
|
+
self.snips_desired = SnipsDesired(snips_desired) if snips_desired else None
|
|
66
|
+
else:
|
|
67
|
+
self.snips_desired = snips_desired
|
|
68
|
+
self._client: httpx.AsyncClient | None = None
|
|
69
|
+
|
|
70
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
71
|
+
if self._client is None:
|
|
72
|
+
headers = {"Content-Type": "application/json"}
|
|
73
|
+
if self.api_key:
|
|
74
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
75
|
+
# Add organization/workspace headers for namespace routing
|
|
76
|
+
if self.organization_id:
|
|
77
|
+
headers["X-Raysurfer-Org-Id"] = self.organization_id
|
|
78
|
+
if self.workspace_id:
|
|
79
|
+
headers["X-Raysurfer-Workspace-Id"] = self.workspace_id
|
|
80
|
+
# Add snippet retrieval scope headers
|
|
81
|
+
if self.public_snips:
|
|
82
|
+
headers["X-Raysurfer-Public-Snips"] = "true"
|
|
83
|
+
if self.snips_desired:
|
|
84
|
+
headers["X-Raysurfer-Snips-Desired"] = self.snips_desired.value
|
|
85
|
+
self._client = httpx.AsyncClient(
|
|
86
|
+
base_url=self.base_url,
|
|
87
|
+
headers=headers,
|
|
88
|
+
timeout=self.timeout,
|
|
89
|
+
)
|
|
90
|
+
return self._client
|
|
91
|
+
|
|
92
|
+
async def close(self) -> None:
|
|
93
|
+
if self._client:
|
|
94
|
+
await self._client.aclose()
|
|
95
|
+
self._client = None
|
|
96
|
+
|
|
97
|
+
async def __aenter__(self) -> "AsyncRaySurfer":
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
101
|
+
await self.close()
|
|
102
|
+
|
|
103
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
104
|
+
client = await self._get_client()
|
|
105
|
+
response = await client.request(method, path, **kwargs)
|
|
106
|
+
|
|
107
|
+
if response.status_code == 401:
|
|
108
|
+
raise AuthenticationError("Invalid API key")
|
|
109
|
+
if response.status_code >= 400:
|
|
110
|
+
raise APIError(response.text, status_code=response.status_code)
|
|
111
|
+
|
|
112
|
+
return response.json()
|
|
113
|
+
|
|
114
|
+
# =========================================================================
|
|
115
|
+
# Store API
|
|
116
|
+
# =========================================================================
|
|
117
|
+
|
|
118
|
+
async def store_code_block(
|
|
119
|
+
self,
|
|
120
|
+
name: str,
|
|
121
|
+
source: str,
|
|
122
|
+
entrypoint: str,
|
|
123
|
+
language: str,
|
|
124
|
+
description: str = "",
|
|
125
|
+
input_schema: dict[str, Any] | None = None,
|
|
126
|
+
output_schema: dict[str, Any] | None = None,
|
|
127
|
+
language_version: str | None = None,
|
|
128
|
+
dependencies: list[str] | None = None,
|
|
129
|
+
tags: list[str] | None = None,
|
|
130
|
+
capabilities: list[str] | None = None,
|
|
131
|
+
example_queries: list[str] | None = None,
|
|
132
|
+
) -> StoreCodeBlockResponse:
|
|
133
|
+
"""Store a new code block"""
|
|
134
|
+
data = {
|
|
135
|
+
"name": name,
|
|
136
|
+
"description": description,
|
|
137
|
+
"source": source,
|
|
138
|
+
"entrypoint": entrypoint,
|
|
139
|
+
"language": language,
|
|
140
|
+
"input_schema": input_schema or {},
|
|
141
|
+
"output_schema": output_schema or {},
|
|
142
|
+
"language_version": language_version,
|
|
143
|
+
"dependencies": dependencies or [],
|
|
144
|
+
"tags": tags or [],
|
|
145
|
+
"capabilities": capabilities or [],
|
|
146
|
+
"example_queries": example_queries,
|
|
147
|
+
}
|
|
148
|
+
result = await self._request("POST", "/api/store/code-block", json=data)
|
|
149
|
+
return StoreCodeBlockResponse(**result)
|
|
150
|
+
|
|
151
|
+
async def store_execution(
|
|
152
|
+
self,
|
|
153
|
+
code_block_id: str,
|
|
154
|
+
triggering_task: str,
|
|
155
|
+
input_data: dict[str, Any],
|
|
156
|
+
output_data: Any,
|
|
157
|
+
execution_state: ExecutionState = ExecutionState.COMPLETED,
|
|
158
|
+
duration_ms: int = 0,
|
|
159
|
+
error_message: str | None = None,
|
|
160
|
+
error_type: str | None = None,
|
|
161
|
+
verdict: AgentVerdict | None = None,
|
|
162
|
+
review: AgentReview | None = None,
|
|
163
|
+
) -> StoreExecutionResponse:
|
|
164
|
+
"""Store an execution record"""
|
|
165
|
+
io = ExecutionIO(
|
|
166
|
+
input_data=input_data,
|
|
167
|
+
output_data=output_data,
|
|
168
|
+
output_type=type(output_data).__name__,
|
|
169
|
+
)
|
|
170
|
+
data = {
|
|
171
|
+
"code_block_id": code_block_id,
|
|
172
|
+
"triggering_task": triggering_task,
|
|
173
|
+
"io": io.model_dump(),
|
|
174
|
+
"execution_state": execution_state.value,
|
|
175
|
+
"duration_ms": duration_ms,
|
|
176
|
+
"error_message": error_message,
|
|
177
|
+
"error_type": error_type,
|
|
178
|
+
"verdict": verdict.value if verdict else None,
|
|
179
|
+
"review": review.model_dump() if review else None,
|
|
180
|
+
}
|
|
181
|
+
result = await self._request("POST", "/api/store/execution", json=data)
|
|
182
|
+
return StoreExecutionResponse(**result)
|
|
183
|
+
|
|
184
|
+
async def submit_execution_result(
|
|
185
|
+
self,
|
|
186
|
+
task: str,
|
|
187
|
+
files_written: list[FileWritten],
|
|
188
|
+
succeeded: bool,
|
|
189
|
+
) -> SubmitExecutionResultResponse:
|
|
190
|
+
"""
|
|
191
|
+
Submit raw execution result - backend handles all processing.
|
|
192
|
+
|
|
193
|
+
This is the simplified API for agent integrations. Just send:
|
|
194
|
+
- The task that was executed
|
|
195
|
+
- Files that were written during execution
|
|
196
|
+
- Whether the task succeeded
|
|
197
|
+
|
|
198
|
+
Backend handles: entrypoint detection, tag extraction, language detection,
|
|
199
|
+
deduplication, quality checks, and storage.
|
|
200
|
+
"""
|
|
201
|
+
data = {
|
|
202
|
+
"task": task,
|
|
203
|
+
"files_written": [f.model_dump() for f in files_written],
|
|
204
|
+
"succeeded": succeeded,
|
|
205
|
+
}
|
|
206
|
+
result = await self._request("POST", "/api/store/execution-result", json=data)
|
|
207
|
+
return SubmitExecutionResultResponse(**result)
|
|
208
|
+
|
|
209
|
+
# =========================================================================
|
|
210
|
+
# Retrieve API
|
|
211
|
+
# =========================================================================
|
|
212
|
+
|
|
213
|
+
async def retrieve(
|
|
214
|
+
self,
|
|
215
|
+
task: str,
|
|
216
|
+
top_k: int = 10,
|
|
217
|
+
min_verdict_score: float = 0.0,
|
|
218
|
+
) -> RetrieveCodeBlockResponse:
|
|
219
|
+
"""Retrieve code blocks by task description (semantic search)"""
|
|
220
|
+
data = {
|
|
221
|
+
"task": task,
|
|
222
|
+
"top_k": top_k,
|
|
223
|
+
"min_verdict_score": min_verdict_score,
|
|
224
|
+
}
|
|
225
|
+
result = await self._request("POST", "/api/retrieve/code-blocks", json=data)
|
|
226
|
+
|
|
227
|
+
code_blocks = [
|
|
228
|
+
CodeBlockMatch(
|
|
229
|
+
code_block=CodeBlock(**cb["code_block"]),
|
|
230
|
+
score=cb["score"],
|
|
231
|
+
verdict_score=cb["verdict_score"],
|
|
232
|
+
thumbs_up=cb["thumbs_up"],
|
|
233
|
+
thumbs_down=cb["thumbs_down"],
|
|
234
|
+
recent_executions=cb.get("recent_executions", []),
|
|
235
|
+
)
|
|
236
|
+
for cb in result["code_blocks"]
|
|
237
|
+
]
|
|
238
|
+
return RetrieveCodeBlockResponse(
|
|
239
|
+
code_blocks=code_blocks,
|
|
240
|
+
total_found=result["total_found"],
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
async def retrieve_best(
|
|
244
|
+
self,
|
|
245
|
+
task: str,
|
|
246
|
+
top_k: int = 10,
|
|
247
|
+
min_verdict_score: float = 0.0,
|
|
248
|
+
) -> RetrieveBestResponse:
|
|
249
|
+
"""Get the single best code block for a task using verdict-aware scoring"""
|
|
250
|
+
data = {
|
|
251
|
+
"task": task,
|
|
252
|
+
"top_k": top_k,
|
|
253
|
+
"min_verdict_score": min_verdict_score,
|
|
254
|
+
}
|
|
255
|
+
result = await self._request("POST", "/api/retrieve/best-for-task", json=data)
|
|
256
|
+
|
|
257
|
+
best_match = None
|
|
258
|
+
if result.get("best_match"):
|
|
259
|
+
bm = result["best_match"]
|
|
260
|
+
best_match = BestMatch(
|
|
261
|
+
code_block=CodeBlock(**bm["code_block"]),
|
|
262
|
+
combined_score=bm["combined_score"],
|
|
263
|
+
vector_score=bm["vector_score"],
|
|
264
|
+
verdict_score=bm["verdict_score"],
|
|
265
|
+
error_resilience=bm["error_resilience"],
|
|
266
|
+
thumbs_up=bm["thumbs_up"],
|
|
267
|
+
thumbs_down=bm["thumbs_down"],
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
alternatives = [AlternativeCandidate(**alt) for alt in result.get("alternative_candidates", [])]
|
|
271
|
+
|
|
272
|
+
return RetrieveBestResponse(
|
|
273
|
+
best_match=best_match,
|
|
274
|
+
alternative_candidates=alternatives,
|
|
275
|
+
retrieval_confidence=result["retrieval_confidence"],
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
async def get_few_shot_examples(
|
|
279
|
+
self,
|
|
280
|
+
task: str,
|
|
281
|
+
k: int = 3,
|
|
282
|
+
) -> list[FewShotExample]:
|
|
283
|
+
"""Retrieve few-shot examples for code generation"""
|
|
284
|
+
data = {"task": task, "k": k}
|
|
285
|
+
result = await self._request("POST", "/api/retrieve/few-shot-examples", json=data)
|
|
286
|
+
return [FewShotExample(**ex) for ex in result["examples"]]
|
|
287
|
+
|
|
288
|
+
async def get_task_patterns(
|
|
289
|
+
self,
|
|
290
|
+
task: str | None = None,
|
|
291
|
+
code_block_id: str | None = None,
|
|
292
|
+
min_thumbs_up: int = 0,
|
|
293
|
+
top_k: int = 20,
|
|
294
|
+
) -> list[TaskPattern]:
|
|
295
|
+
"""Retrieve proven task→code mappings"""
|
|
296
|
+
data = {
|
|
297
|
+
"task": task,
|
|
298
|
+
"code_block_id": code_block_id,
|
|
299
|
+
"min_thumbs_up": min_thumbs_up,
|
|
300
|
+
"top_k": top_k,
|
|
301
|
+
}
|
|
302
|
+
result = await self._request("POST", "/api/retrieve/task-patterns", json=data)
|
|
303
|
+
return [TaskPattern(**p) for p in result["patterns"]]
|
|
304
|
+
|
|
305
|
+
async def get_code_files(
|
|
306
|
+
self,
|
|
307
|
+
task: str,
|
|
308
|
+
top_k: int = 5,
|
|
309
|
+
min_verdict_score: float = 0.3,
|
|
310
|
+
prefer_complete: bool = True,
|
|
311
|
+
) -> GetCodeFilesResponse:
|
|
312
|
+
"""
|
|
313
|
+
Get code files for a task, ready to download to sandbox.
|
|
314
|
+
|
|
315
|
+
Returns code blocks with full source code, optimized for:
|
|
316
|
+
- High verdict scores (proven to work)
|
|
317
|
+
- More complete implementations (prefer longer source)
|
|
318
|
+
- Task relevance (semantic similarity)
|
|
319
|
+
"""
|
|
320
|
+
data = {
|
|
321
|
+
"task": task,
|
|
322
|
+
"top_k": top_k,
|
|
323
|
+
"min_verdict_score": min_verdict_score,
|
|
324
|
+
"prefer_complete": prefer_complete,
|
|
325
|
+
}
|
|
326
|
+
result = await self._request("POST", "/api/retrieve/code-files", json=data)
|
|
327
|
+
return GetCodeFilesResponse(
|
|
328
|
+
files=[CodeFile(**f) for f in result["files"]],
|
|
329
|
+
task=result["task"],
|
|
330
|
+
total_found=result["total_found"],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
async def record_cache_usage(
|
|
334
|
+
self,
|
|
335
|
+
task: str,
|
|
336
|
+
code_block_id: str,
|
|
337
|
+
code_block_name: str,
|
|
338
|
+
code_block_description: str,
|
|
339
|
+
succeeded: bool,
|
|
340
|
+
) -> dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Record that a cached code block was used for a task.
|
|
343
|
+
|
|
344
|
+
This triggers background voting to assess whether the cached code
|
|
345
|
+
actually helped complete the task successfully.
|
|
346
|
+
"""
|
|
347
|
+
data = {
|
|
348
|
+
"task": task,
|
|
349
|
+
"code_block_id": code_block_id,
|
|
350
|
+
"code_block_name": code_block_name,
|
|
351
|
+
"code_block_description": code_block_description,
|
|
352
|
+
"succeeded": succeeded,
|
|
353
|
+
}
|
|
354
|
+
return await self._request("POST", "/api/store/cache-usage", json=data)
|
|
355
|
+
|
|
356
|
+
# =========================================================================
|
|
357
|
+
# Simplified API (aliases)
|
|
358
|
+
# =========================================================================
|
|
359
|
+
|
|
360
|
+
async def get_code_snips(
|
|
361
|
+
self,
|
|
362
|
+
task: str,
|
|
363
|
+
top_k: int = 10,
|
|
364
|
+
min_verdict_score: float = 0.0,
|
|
365
|
+
) -> RetrieveCodeBlockResponse:
|
|
366
|
+
"""
|
|
367
|
+
Get cached code snippets for a task.
|
|
368
|
+
|
|
369
|
+
Alias for retrieve() - searches for code blocks by task description.
|
|
370
|
+
"""
|
|
371
|
+
return await self.retrieve(task, top_k, min_verdict_score)
|
|
372
|
+
|
|
373
|
+
async def upload_new_code_snips(
|
|
374
|
+
self,
|
|
375
|
+
task: str,
|
|
376
|
+
files_written: list[FileWritten],
|
|
377
|
+
succeeded: bool,
|
|
378
|
+
) -> SubmitExecutionResultResponse:
|
|
379
|
+
"""
|
|
380
|
+
Upload new code snippets from an execution.
|
|
381
|
+
|
|
382
|
+
Alias for submit_execution_result() - stores code files for future reuse.
|
|
383
|
+
"""
|
|
384
|
+
return await self.submit_execution_result(task, files_written, succeeded)
|
|
385
|
+
|
|
386
|
+
async def vote_code_snip(
|
|
387
|
+
self,
|
|
388
|
+
task: str,
|
|
389
|
+
code_block_id: str,
|
|
390
|
+
code_block_name: str,
|
|
391
|
+
code_block_description: str,
|
|
392
|
+
succeeded: bool,
|
|
393
|
+
) -> dict[str, Any]:
|
|
394
|
+
"""
|
|
395
|
+
Vote on whether a cached code snippet was useful.
|
|
396
|
+
|
|
397
|
+
Alias for record_cache_usage() - triggers background voting.
|
|
398
|
+
"""
|
|
399
|
+
return await self.record_cache_usage(
|
|
400
|
+
task, code_block_id, code_block_name, code_block_description, succeeded
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class RaySurfer:
|
|
405
|
+
"""Sync client for RaySurfer API"""
|
|
406
|
+
|
|
407
|
+
def __init__(
|
|
408
|
+
self,
|
|
409
|
+
api_key: str | None = None,
|
|
410
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
411
|
+
timeout: float = 60.0,
|
|
412
|
+
organization_id: str | None = None,
|
|
413
|
+
workspace_id: str | None = None,
|
|
414
|
+
public_snips: bool = False,
|
|
415
|
+
snips_desired: SnipsDesired | str | None = None,
|
|
416
|
+
):
|
|
417
|
+
"""
|
|
418
|
+
Initialize the RaySurfer sync client.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
api_key: RaySurfer API key (or set RAYSURFER_API_KEY env var)
|
|
422
|
+
base_url: API base URL
|
|
423
|
+
timeout: Request timeout in seconds
|
|
424
|
+
organization_id: Optional organization ID for dedicated namespace (team/enterprise)
|
|
425
|
+
workspace_id: Optional workspace ID for client-specific namespace (enterprise only)
|
|
426
|
+
public_snips: Whether to include public/shared snippets in retrieval (default: False)
|
|
427
|
+
snips_desired: Scope of private snippets - "company" (Team/Enterprise) or "client" (Enterprise only)
|
|
428
|
+
"""
|
|
429
|
+
self.api_key = api_key
|
|
430
|
+
self.base_url = base_url.rstrip("/")
|
|
431
|
+
self.timeout = timeout
|
|
432
|
+
self.organization_id = organization_id
|
|
433
|
+
self.workspace_id = workspace_id
|
|
434
|
+
self.public_snips = public_snips
|
|
435
|
+
# Convert string to SnipsDesired if needed
|
|
436
|
+
if isinstance(snips_desired, str):
|
|
437
|
+
self.snips_desired = SnipsDesired(snips_desired) if snips_desired else None
|
|
438
|
+
else:
|
|
439
|
+
self.snips_desired = snips_desired
|
|
440
|
+
self._client: httpx.Client | None = None
|
|
441
|
+
|
|
442
|
+
def _get_client(self) -> httpx.Client:
|
|
443
|
+
if self._client is None:
|
|
444
|
+
headers = {"Content-Type": "application/json"}
|
|
445
|
+
if self.api_key:
|
|
446
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
447
|
+
# Add organization/workspace headers for namespace routing
|
|
448
|
+
if self.organization_id:
|
|
449
|
+
headers["X-Raysurfer-Org-Id"] = self.organization_id
|
|
450
|
+
if self.workspace_id:
|
|
451
|
+
headers["X-Raysurfer-Workspace-Id"] = self.workspace_id
|
|
452
|
+
# Add snippet retrieval scope headers
|
|
453
|
+
if self.public_snips:
|
|
454
|
+
headers["X-Raysurfer-Public-Snips"] = "true"
|
|
455
|
+
if self.snips_desired:
|
|
456
|
+
headers["X-Raysurfer-Snips-Desired"] = self.snips_desired.value
|
|
457
|
+
self._client = httpx.Client(
|
|
458
|
+
base_url=self.base_url,
|
|
459
|
+
headers=headers,
|
|
460
|
+
timeout=self.timeout,
|
|
461
|
+
)
|
|
462
|
+
return self._client
|
|
463
|
+
|
|
464
|
+
def close(self) -> None:
|
|
465
|
+
if self._client:
|
|
466
|
+
self._client.close()
|
|
467
|
+
self._client = None
|
|
468
|
+
|
|
469
|
+
def __enter__(self) -> "RaySurfer":
|
|
470
|
+
return self
|
|
471
|
+
|
|
472
|
+
def __exit__(self, *args: Any) -> None:
|
|
473
|
+
self.close()
|
|
474
|
+
|
|
475
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
476
|
+
client = self._get_client()
|
|
477
|
+
response = client.request(method, path, **kwargs)
|
|
478
|
+
|
|
479
|
+
if response.status_code == 401:
|
|
480
|
+
raise AuthenticationError("Invalid API key")
|
|
481
|
+
if response.status_code >= 400:
|
|
482
|
+
raise APIError(response.text, status_code=response.status_code)
|
|
483
|
+
|
|
484
|
+
return response.json()
|
|
485
|
+
|
|
486
|
+
# =========================================================================
|
|
487
|
+
# Store API
|
|
488
|
+
# =========================================================================
|
|
489
|
+
|
|
490
|
+
def store_code_block(
|
|
491
|
+
self,
|
|
492
|
+
name: str,
|
|
493
|
+
source: str,
|
|
494
|
+
entrypoint: str,
|
|
495
|
+
language: str,
|
|
496
|
+
description: str = "",
|
|
497
|
+
input_schema: dict[str, Any] | None = None,
|
|
498
|
+
output_schema: dict[str, Any] | None = None,
|
|
499
|
+
language_version: str | None = None,
|
|
500
|
+
dependencies: list[str] | None = None,
|
|
501
|
+
tags: list[str] | None = None,
|
|
502
|
+
capabilities: list[str] | None = None,
|
|
503
|
+
example_queries: list[str] | None = None,
|
|
504
|
+
) -> StoreCodeBlockResponse:
|
|
505
|
+
"""Store a new code block"""
|
|
506
|
+
data = {
|
|
507
|
+
"name": name,
|
|
508
|
+
"description": description,
|
|
509
|
+
"source": source,
|
|
510
|
+
"entrypoint": entrypoint,
|
|
511
|
+
"language": language,
|
|
512
|
+
"input_schema": input_schema or {},
|
|
513
|
+
"output_schema": output_schema or {},
|
|
514
|
+
"language_version": language_version,
|
|
515
|
+
"dependencies": dependencies or [],
|
|
516
|
+
"tags": tags or [],
|
|
517
|
+
"capabilities": capabilities or [],
|
|
518
|
+
"example_queries": example_queries,
|
|
519
|
+
}
|
|
520
|
+
result = self._request("POST", "/api/store/code-block", json=data)
|
|
521
|
+
return StoreCodeBlockResponse(**result)
|
|
522
|
+
|
|
523
|
+
def store_execution(
|
|
524
|
+
self,
|
|
525
|
+
code_block_id: str,
|
|
526
|
+
triggering_task: str,
|
|
527
|
+
input_data: dict[str, Any],
|
|
528
|
+
output_data: Any,
|
|
529
|
+
execution_state: ExecutionState = ExecutionState.COMPLETED,
|
|
530
|
+
duration_ms: int = 0,
|
|
531
|
+
error_message: str | None = None,
|
|
532
|
+
error_type: str | None = None,
|
|
533
|
+
verdict: AgentVerdict | None = None,
|
|
534
|
+
review: AgentReview | None = None,
|
|
535
|
+
) -> StoreExecutionResponse:
|
|
536
|
+
"""Store an execution record"""
|
|
537
|
+
io = ExecutionIO(
|
|
538
|
+
input_data=input_data,
|
|
539
|
+
output_data=output_data,
|
|
540
|
+
output_type=type(output_data).__name__,
|
|
541
|
+
)
|
|
542
|
+
data = {
|
|
543
|
+
"code_block_id": code_block_id,
|
|
544
|
+
"triggering_task": triggering_task,
|
|
545
|
+
"io": io.model_dump(),
|
|
546
|
+
"execution_state": execution_state.value,
|
|
547
|
+
"duration_ms": duration_ms,
|
|
548
|
+
"error_message": error_message,
|
|
549
|
+
"error_type": error_type,
|
|
550
|
+
"verdict": verdict.value if verdict else None,
|
|
551
|
+
"review": review.model_dump() if review else None,
|
|
552
|
+
}
|
|
553
|
+
result = self._request("POST", "/api/store/execution", json=data)
|
|
554
|
+
return StoreExecutionResponse(**result)
|
|
555
|
+
|
|
556
|
+
def submit_execution_result(
|
|
557
|
+
self,
|
|
558
|
+
task: str,
|
|
559
|
+
files_written: list[FileWritten],
|
|
560
|
+
succeeded: bool,
|
|
561
|
+
) -> SubmitExecutionResultResponse:
|
|
562
|
+
"""
|
|
563
|
+
Submit raw execution result - backend handles all processing.
|
|
564
|
+
|
|
565
|
+
This is the simplified API for agent integrations. Just send:
|
|
566
|
+
- The task that was executed
|
|
567
|
+
- Files that were written during execution
|
|
568
|
+
- Whether the task succeeded
|
|
569
|
+
|
|
570
|
+
Backend handles: entrypoint detection, tag extraction, language detection,
|
|
571
|
+
deduplication, quality checks, and storage.
|
|
572
|
+
"""
|
|
573
|
+
data = {
|
|
574
|
+
"task": task,
|
|
575
|
+
"files_written": [f.model_dump() for f in files_written],
|
|
576
|
+
"succeeded": succeeded,
|
|
577
|
+
}
|
|
578
|
+
result = self._request("POST", "/api/store/execution-result", json=data)
|
|
579
|
+
return SubmitExecutionResultResponse(**result)
|
|
580
|
+
|
|
581
|
+
# =========================================================================
|
|
582
|
+
# Retrieve API
|
|
583
|
+
# =========================================================================
|
|
584
|
+
|
|
585
|
+
def retrieve(
|
|
586
|
+
self,
|
|
587
|
+
task: str,
|
|
588
|
+
top_k: int = 10,
|
|
589
|
+
min_verdict_score: float = 0.0,
|
|
590
|
+
) -> RetrieveCodeBlockResponse:
|
|
591
|
+
"""Retrieve code blocks by task description (semantic search)"""
|
|
592
|
+
data = {
|
|
593
|
+
"task": task,
|
|
594
|
+
"top_k": top_k,
|
|
595
|
+
"min_verdict_score": min_verdict_score,
|
|
596
|
+
}
|
|
597
|
+
result = self._request("POST", "/api/retrieve/code-blocks", json=data)
|
|
598
|
+
|
|
599
|
+
code_blocks = [
|
|
600
|
+
CodeBlockMatch(
|
|
601
|
+
code_block=CodeBlock(**cb["code_block"]),
|
|
602
|
+
score=cb["score"],
|
|
603
|
+
verdict_score=cb["verdict_score"],
|
|
604
|
+
thumbs_up=cb["thumbs_up"],
|
|
605
|
+
thumbs_down=cb["thumbs_down"],
|
|
606
|
+
recent_executions=cb.get("recent_executions", []),
|
|
607
|
+
)
|
|
608
|
+
for cb in result["code_blocks"]
|
|
609
|
+
]
|
|
610
|
+
return RetrieveCodeBlockResponse(
|
|
611
|
+
code_blocks=code_blocks,
|
|
612
|
+
total_found=result["total_found"],
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
def retrieve_best(
|
|
616
|
+
self,
|
|
617
|
+
task: str,
|
|
618
|
+
top_k: int = 10,
|
|
619
|
+
min_verdict_score: float = 0.0,
|
|
620
|
+
) -> RetrieveBestResponse:
|
|
621
|
+
"""Get the single best code block for a task using verdict-aware scoring"""
|
|
622
|
+
data = {
|
|
623
|
+
"task": task,
|
|
624
|
+
"top_k": top_k,
|
|
625
|
+
"min_verdict_score": min_verdict_score,
|
|
626
|
+
}
|
|
627
|
+
result = self._request("POST", "/api/retrieve/best-for-task", json=data)
|
|
628
|
+
|
|
629
|
+
best_match = None
|
|
630
|
+
if result.get("best_match"):
|
|
631
|
+
bm = result["best_match"]
|
|
632
|
+
best_match = BestMatch(
|
|
633
|
+
code_block=CodeBlock(**bm["code_block"]),
|
|
634
|
+
combined_score=bm["combined_score"],
|
|
635
|
+
vector_score=bm["vector_score"],
|
|
636
|
+
verdict_score=bm["verdict_score"],
|
|
637
|
+
error_resilience=bm["error_resilience"],
|
|
638
|
+
thumbs_up=bm["thumbs_up"],
|
|
639
|
+
thumbs_down=bm["thumbs_down"],
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
alternatives = [AlternativeCandidate(**alt) for alt in result.get("alternative_candidates", [])]
|
|
643
|
+
|
|
644
|
+
return RetrieveBestResponse(
|
|
645
|
+
best_match=best_match,
|
|
646
|
+
alternative_candidates=alternatives,
|
|
647
|
+
retrieval_confidence=result["retrieval_confidence"],
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
def get_few_shot_examples(
|
|
651
|
+
self,
|
|
652
|
+
task: str,
|
|
653
|
+
k: int = 3,
|
|
654
|
+
) -> list[FewShotExample]:
|
|
655
|
+
"""Retrieve few-shot examples for code generation"""
|
|
656
|
+
data = {"task": task, "k": k}
|
|
657
|
+
result = self._request("POST", "/api/retrieve/few-shot-examples", json=data)
|
|
658
|
+
return [FewShotExample(**ex) for ex in result["examples"]]
|
|
659
|
+
|
|
660
|
+
def get_task_patterns(
|
|
661
|
+
self,
|
|
662
|
+
task: str | None = None,
|
|
663
|
+
code_block_id: str | None = None,
|
|
664
|
+
min_thumbs_up: int = 0,
|
|
665
|
+
top_k: int = 20,
|
|
666
|
+
) -> list[TaskPattern]:
|
|
667
|
+
"""Retrieve proven task→code mappings"""
|
|
668
|
+
data = {
|
|
669
|
+
"task": task,
|
|
670
|
+
"code_block_id": code_block_id,
|
|
671
|
+
"min_thumbs_up": min_thumbs_up,
|
|
672
|
+
"top_k": top_k,
|
|
673
|
+
}
|
|
674
|
+
result = self._request("POST", "/api/retrieve/task-patterns", json=data)
|
|
675
|
+
return [TaskPattern(**p) for p in result["patterns"]]
|
|
676
|
+
|
|
677
|
+
def get_code_files(
|
|
678
|
+
self,
|
|
679
|
+
task: str,
|
|
680
|
+
top_k: int = 5,
|
|
681
|
+
min_verdict_score: float = 0.3,
|
|
682
|
+
prefer_complete: bool = True,
|
|
683
|
+
) -> GetCodeFilesResponse:
|
|
684
|
+
"""
|
|
685
|
+
Get code files for a task, ready to download to sandbox.
|
|
686
|
+
|
|
687
|
+
Returns code blocks with full source code, optimized for:
|
|
688
|
+
- High verdict scores (proven to work)
|
|
689
|
+
- More complete implementations (prefer longer source)
|
|
690
|
+
- Task relevance (semantic similarity)
|
|
691
|
+
"""
|
|
692
|
+
data = {
|
|
693
|
+
"task": task,
|
|
694
|
+
"top_k": top_k,
|
|
695
|
+
"min_verdict_score": min_verdict_score,
|
|
696
|
+
"prefer_complete": prefer_complete,
|
|
697
|
+
}
|
|
698
|
+
result = self._request("POST", "/api/retrieve/code-files", json=data)
|
|
699
|
+
return GetCodeFilesResponse(
|
|
700
|
+
files=[CodeFile(**f) for f in result["files"]],
|
|
701
|
+
task=result["task"],
|
|
702
|
+
total_found=result["total_found"],
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
def record_cache_usage(
|
|
706
|
+
self,
|
|
707
|
+
task: str,
|
|
708
|
+
code_block_id: str,
|
|
709
|
+
code_block_name: str,
|
|
710
|
+
code_block_description: str,
|
|
711
|
+
succeeded: bool,
|
|
712
|
+
) -> dict[str, Any]:
|
|
713
|
+
"""
|
|
714
|
+
Record that a cached code block was used for a task.
|
|
715
|
+
|
|
716
|
+
This triggers background voting to assess whether the cached code
|
|
717
|
+
actually helped complete the task successfully.
|
|
718
|
+
"""
|
|
719
|
+
data = {
|
|
720
|
+
"task": task,
|
|
721
|
+
"code_block_id": code_block_id,
|
|
722
|
+
"code_block_name": code_block_name,
|
|
723
|
+
"code_block_description": code_block_description,
|
|
724
|
+
"succeeded": succeeded,
|
|
725
|
+
}
|
|
726
|
+
return self._request("POST", "/api/store/cache-usage", json=data)
|
|
727
|
+
|
|
728
|
+
# =========================================================================
|
|
729
|
+
# Simplified API (aliases)
|
|
730
|
+
# =========================================================================
|
|
731
|
+
|
|
732
|
+
def get_code_snips(
|
|
733
|
+
self,
|
|
734
|
+
task: str,
|
|
735
|
+
top_k: int = 10,
|
|
736
|
+
min_verdict_score: float = 0.0,
|
|
737
|
+
) -> RetrieveCodeBlockResponse:
|
|
738
|
+
"""
|
|
739
|
+
Get cached code snippets for a task.
|
|
740
|
+
|
|
741
|
+
Alias for retrieve() - searches for code blocks by task description.
|
|
742
|
+
"""
|
|
743
|
+
return self.retrieve(task, top_k, min_verdict_score)
|
|
744
|
+
|
|
745
|
+
def upload_new_code_snips(
|
|
746
|
+
self,
|
|
747
|
+
task: str,
|
|
748
|
+
files_written: list[FileWritten],
|
|
749
|
+
succeeded: bool,
|
|
750
|
+
) -> SubmitExecutionResultResponse:
|
|
751
|
+
"""
|
|
752
|
+
Upload new code snippets from an execution.
|
|
753
|
+
|
|
754
|
+
Alias for submit_execution_result() - stores code files for future reuse.
|
|
755
|
+
"""
|
|
756
|
+
return self.submit_execution_result(task, files_written, succeeded)
|
|
757
|
+
|
|
758
|
+
def vote_code_snip(
|
|
759
|
+
self,
|
|
760
|
+
task: str,
|
|
761
|
+
code_block_id: str,
|
|
762
|
+
code_block_name: str,
|
|
763
|
+
code_block_description: str,
|
|
764
|
+
succeeded: bool,
|
|
765
|
+
) -> dict[str, Any]:
|
|
766
|
+
"""
|
|
767
|
+
Vote on whether a cached code snippet was useful.
|
|
768
|
+
|
|
769
|
+
Alias for record_cache_usage() - triggers background voting.
|
|
770
|
+
"""
|
|
771
|
+
return self.record_cache_usage(
|
|
772
|
+
task, code_block_id, code_block_name, code_block_description, succeeded
|
|
773
|
+
)
|