raysurfer 0.6.3__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 +111 -0
- raysurfer/_version.py +1 -0
- raysurfer/client.py +1009 -0
- raysurfer/exceptions.py +43 -0
- raysurfer/sdk_client.py +686 -0
- raysurfer/sdk_types.py +33 -0
- raysurfer/types.py +216 -0
- raysurfer-0.6.3.dist-info/METADATA +210 -0
- raysurfer-0.6.3.dist-info/RECORD +10 -0
- raysurfer-0.6.3.dist-info/WHEEL +4 -0
raysurfer/client.py
ADDED
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
"""RaySurfer SDK client"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from raysurfer._version import __version__
|
|
10
|
+
from raysurfer.exceptions import (
|
|
11
|
+
APIError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
CacheUnavailableError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
)
|
|
16
|
+
from raysurfer.sdk_types import CodeFile, GetCodeFilesResponse
|
|
17
|
+
from raysurfer.types import (
|
|
18
|
+
AgentReview,
|
|
19
|
+
AgentVerdict,
|
|
20
|
+
AlternativeCandidate,
|
|
21
|
+
AutoReviewResponse,
|
|
22
|
+
BestMatch,
|
|
23
|
+
CodeBlock,
|
|
24
|
+
CodeBlockMatch,
|
|
25
|
+
ExecutionIO,
|
|
26
|
+
ExecutionRecord,
|
|
27
|
+
ExecutionState,
|
|
28
|
+
FewShotExample,
|
|
29
|
+
FileWritten,
|
|
30
|
+
RetrieveBestResponse,
|
|
31
|
+
RetrieveCodeBlockResponse,
|
|
32
|
+
RetrieveExecutionsResponse,
|
|
33
|
+
SnipsDesired,
|
|
34
|
+
StoreCodeBlockResponse,
|
|
35
|
+
StoreExecutionResponse,
|
|
36
|
+
SubmitExecutionResultResponse,
|
|
37
|
+
TaskPattern,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
DEFAULT_BASE_URL = "https://api.raysurfer.com"
|
|
43
|
+
|
|
44
|
+
# Maximum number of retry attempts for transient failures
|
|
45
|
+
MAX_RETRIES = 3
|
|
46
|
+
# Base delay in seconds for exponential backoff
|
|
47
|
+
RETRY_BASE_DELAY = 0.5
|
|
48
|
+
# HTTP status codes that should trigger a retry
|
|
49
|
+
RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AsyncRaySurfer:
|
|
53
|
+
"""Async client for RaySurfer API"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
api_key: str | None = None,
|
|
58
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
59
|
+
timeout: float = 60.0,
|
|
60
|
+
organization_id: str | None = None,
|
|
61
|
+
workspace_id: str | None = None,
|
|
62
|
+
snips_desired: SnipsDesired | str | None = None,
|
|
63
|
+
namespace: str | None = None,
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Initialize the RaySurfer async client.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
api_key: RaySurfer API key (or set RAYSURFER_API_KEY env var)
|
|
70
|
+
base_url: API base URL
|
|
71
|
+
timeout: Request timeout in seconds
|
|
72
|
+
organization_id: Optional organization ID for dedicated namespace (team/enterprise)
|
|
73
|
+
workspace_id: Optional workspace ID for client-specific namespace (enterprise only)
|
|
74
|
+
snips_desired: Scope of private snippets - "company" (Team/Enterprise) or "client" (Enterprise only)
|
|
75
|
+
namespace: Custom namespace for code storage/retrieval (overrides org-based namespacing)
|
|
76
|
+
"""
|
|
77
|
+
self.api_key = api_key
|
|
78
|
+
self.base_url = base_url.rstrip("/")
|
|
79
|
+
self.timeout = timeout
|
|
80
|
+
self.organization_id = organization_id
|
|
81
|
+
self.workspace_id = workspace_id
|
|
82
|
+
self.namespace = namespace
|
|
83
|
+
# Convert string to SnipsDesired if needed
|
|
84
|
+
if isinstance(snips_desired, str):
|
|
85
|
+
self.snips_desired = SnipsDesired(snips_desired) if snips_desired else None
|
|
86
|
+
else:
|
|
87
|
+
self.snips_desired = snips_desired
|
|
88
|
+
self._client: httpx.AsyncClient | None = None
|
|
89
|
+
|
|
90
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
91
|
+
if self._client is None:
|
|
92
|
+
headers = {"Content-Type": "application/json"}
|
|
93
|
+
if self.api_key:
|
|
94
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
95
|
+
# Add organization/workspace headers for namespace routing
|
|
96
|
+
if self.organization_id:
|
|
97
|
+
headers["X-Raysurfer-Org-Id"] = self.organization_id
|
|
98
|
+
if self.workspace_id:
|
|
99
|
+
headers["X-Raysurfer-Workspace-Id"] = self.workspace_id
|
|
100
|
+
# Add snippet retrieval scope headers
|
|
101
|
+
if self.snips_desired:
|
|
102
|
+
headers["X-Raysurfer-Snips-Desired"] = self.snips_desired.value
|
|
103
|
+
# Custom namespace override
|
|
104
|
+
if self.namespace:
|
|
105
|
+
headers["X-Raysurfer-Namespace"] = self.namespace
|
|
106
|
+
# SDK version for tracking
|
|
107
|
+
headers["X-Raysurfer-SDK-Version"] = f"python/{__version__}"
|
|
108
|
+
self._client = httpx.AsyncClient(
|
|
109
|
+
base_url=self.base_url,
|
|
110
|
+
headers=headers,
|
|
111
|
+
timeout=self.timeout,
|
|
112
|
+
)
|
|
113
|
+
return self._client
|
|
114
|
+
|
|
115
|
+
async def close(self) -> None:
|
|
116
|
+
if self._client:
|
|
117
|
+
await self._client.aclose()
|
|
118
|
+
self._client = None
|
|
119
|
+
|
|
120
|
+
async def __aenter__(self) -> "AsyncRaySurfer":
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
124
|
+
await self.close()
|
|
125
|
+
|
|
126
|
+
async def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
127
|
+
client = await self._get_client()
|
|
128
|
+
last_exception: Exception | None = None
|
|
129
|
+
|
|
130
|
+
for attempt in range(MAX_RETRIES):
|
|
131
|
+
try:
|
|
132
|
+
response = await client.request(method, path, **kwargs)
|
|
133
|
+
|
|
134
|
+
if response.status_code == 401:
|
|
135
|
+
raise AuthenticationError("Invalid API key")
|
|
136
|
+
if response.status_code == 429:
|
|
137
|
+
retry_after = response.headers.get("Retry-After")
|
|
138
|
+
delay = float(retry_after) if retry_after else RETRY_BASE_DELAY * (2**attempt)
|
|
139
|
+
if attempt < MAX_RETRIES - 1:
|
|
140
|
+
logger.warning(f"Rate limited, retrying in {delay:.1f}s (attempt {attempt + 1}/{MAX_RETRIES})")
|
|
141
|
+
await asyncio.sleep(delay)
|
|
142
|
+
continue
|
|
143
|
+
raise RateLimitError(retry_after=delay)
|
|
144
|
+
if response.status_code in RETRYABLE_STATUS_CODES:
|
|
145
|
+
if attempt < MAX_RETRIES - 1:
|
|
146
|
+
delay = RETRY_BASE_DELAY * (2**attempt)
|
|
147
|
+
logger.warning(
|
|
148
|
+
f"Server error {response.status_code}, retrying in {delay:.1f}s (attempt {attempt + 1}/{MAX_RETRIES})"
|
|
149
|
+
)
|
|
150
|
+
await asyncio.sleep(delay)
|
|
151
|
+
continue
|
|
152
|
+
if response.status_code >= 400:
|
|
153
|
+
raise APIError(response.text, status_code=response.status_code)
|
|
154
|
+
|
|
155
|
+
return response.json()
|
|
156
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
|
157
|
+
last_exception = e
|
|
158
|
+
if attempt < MAX_RETRIES - 1:
|
|
159
|
+
delay = RETRY_BASE_DELAY * (2**attempt)
|
|
160
|
+
logger.warning(
|
|
161
|
+
f"Network error: {e}, retrying in {delay:.1f}s (attempt {attempt + 1}/{MAX_RETRIES})"
|
|
162
|
+
)
|
|
163
|
+
await asyncio.sleep(delay)
|
|
164
|
+
continue
|
|
165
|
+
raise CacheUnavailableError(f"Failed to connect after {MAX_RETRIES} attempts: {e}") from e
|
|
166
|
+
|
|
167
|
+
# Should not reach here, but just in case
|
|
168
|
+
raise CacheUnavailableError(f"Request failed after {MAX_RETRIES} attempts") from last_exception
|
|
169
|
+
|
|
170
|
+
# =========================================================================
|
|
171
|
+
# Store API
|
|
172
|
+
# =========================================================================
|
|
173
|
+
|
|
174
|
+
async def store_code_block(
|
|
175
|
+
self,
|
|
176
|
+
name: str,
|
|
177
|
+
source: str,
|
|
178
|
+
entrypoint: str,
|
|
179
|
+
language: str,
|
|
180
|
+
description: str = "",
|
|
181
|
+
input_schema: dict[str, Any] | None = None,
|
|
182
|
+
output_schema: dict[str, Any] | None = None,
|
|
183
|
+
language_version: str | None = None,
|
|
184
|
+
dependencies: list[str] | None = None,
|
|
185
|
+
tags: list[str] | None = None,
|
|
186
|
+
capabilities: list[str] | None = None,
|
|
187
|
+
example_queries: list[str] | None = None,
|
|
188
|
+
) -> StoreCodeBlockResponse:
|
|
189
|
+
"""Store a new code block"""
|
|
190
|
+
data = {
|
|
191
|
+
"name": name,
|
|
192
|
+
"description": description,
|
|
193
|
+
"source": source,
|
|
194
|
+
"entrypoint": entrypoint,
|
|
195
|
+
"language": language,
|
|
196
|
+
"input_schema": input_schema or {},
|
|
197
|
+
"output_schema": output_schema or {},
|
|
198
|
+
"language_version": language_version,
|
|
199
|
+
"dependencies": dependencies or [],
|
|
200
|
+
"tags": tags or [],
|
|
201
|
+
"capabilities": capabilities or [],
|
|
202
|
+
"example_queries": example_queries,
|
|
203
|
+
}
|
|
204
|
+
result = await self._request("POST", "/api/store/code-block", json=data)
|
|
205
|
+
return StoreCodeBlockResponse(**result)
|
|
206
|
+
|
|
207
|
+
async def store_execution(
|
|
208
|
+
self,
|
|
209
|
+
code_block_id: str,
|
|
210
|
+
triggering_task: str,
|
|
211
|
+
input_data: dict[str, Any],
|
|
212
|
+
output_data: Any,
|
|
213
|
+
execution_state: ExecutionState = ExecutionState.COMPLETED,
|
|
214
|
+
duration_ms: int = 0,
|
|
215
|
+
error_message: str | None = None,
|
|
216
|
+
error_type: str | None = None,
|
|
217
|
+
verdict: AgentVerdict | None = None,
|
|
218
|
+
review: AgentReview | None = None,
|
|
219
|
+
) -> StoreExecutionResponse:
|
|
220
|
+
"""Store an execution record"""
|
|
221
|
+
io = ExecutionIO(
|
|
222
|
+
input_data=input_data,
|
|
223
|
+
output_data=output_data,
|
|
224
|
+
output_type=type(output_data).__name__,
|
|
225
|
+
)
|
|
226
|
+
data = {
|
|
227
|
+
"code_block_id": code_block_id,
|
|
228
|
+
"triggering_task": triggering_task,
|
|
229
|
+
"io": io.model_dump(),
|
|
230
|
+
"execution_state": execution_state.value,
|
|
231
|
+
"duration_ms": duration_ms,
|
|
232
|
+
"error_message": error_message,
|
|
233
|
+
"error_type": error_type,
|
|
234
|
+
"verdict": verdict.value if verdict else None,
|
|
235
|
+
"review": review.model_dump() if review else None,
|
|
236
|
+
}
|
|
237
|
+
result = await self._request("POST", "/api/store/execution", json=data)
|
|
238
|
+
return StoreExecutionResponse(**result)
|
|
239
|
+
|
|
240
|
+
async def upload_new_code_snips(
|
|
241
|
+
self,
|
|
242
|
+
task: str,
|
|
243
|
+
files_written: list[FileWritten],
|
|
244
|
+
succeeded: bool,
|
|
245
|
+
auto_vote: bool = True,
|
|
246
|
+
execution_logs: str | None = None,
|
|
247
|
+
) -> SubmitExecutionResultResponse:
|
|
248
|
+
"""
|
|
249
|
+
Upload new code snippets from an execution.
|
|
250
|
+
|
|
251
|
+
This is the simplified API for agent integrations. Just send:
|
|
252
|
+
- The task that was executed
|
|
253
|
+
- Files that were written during execution
|
|
254
|
+
- Whether the task succeeded
|
|
255
|
+
- Whether to auto-vote on stored blocks (default: True)
|
|
256
|
+
- Captured execution logs for vote context
|
|
257
|
+
|
|
258
|
+
Backend handles: entrypoint detection, tag extraction, language detection,
|
|
259
|
+
deduplication, quality checks, and storage.
|
|
260
|
+
"""
|
|
261
|
+
data: dict[str, Any] = {
|
|
262
|
+
"task": task,
|
|
263
|
+
"files_written": [f.model_dump() for f in files_written],
|
|
264
|
+
"succeeded": succeeded,
|
|
265
|
+
"auto_vote": auto_vote,
|
|
266
|
+
}
|
|
267
|
+
if execution_logs is not None:
|
|
268
|
+
data["execution_logs"] = execution_logs
|
|
269
|
+
result = await self._request("POST", "/api/store/execution-result", json=data)
|
|
270
|
+
return SubmitExecutionResultResponse(**result)
|
|
271
|
+
|
|
272
|
+
# =========================================================================
|
|
273
|
+
# Retrieve API
|
|
274
|
+
# =========================================================================
|
|
275
|
+
|
|
276
|
+
async def get_code_snips(
|
|
277
|
+
self,
|
|
278
|
+
task: str,
|
|
279
|
+
top_k: int = 10,
|
|
280
|
+
min_verdict_score: float = 0.0,
|
|
281
|
+
) -> RetrieveCodeBlockResponse:
|
|
282
|
+
"""
|
|
283
|
+
Get cached code snippets for a task.
|
|
284
|
+
|
|
285
|
+
Searches for code blocks by task description using semantic search.
|
|
286
|
+
"""
|
|
287
|
+
data = {
|
|
288
|
+
"task": task,
|
|
289
|
+
"top_k": top_k,
|
|
290
|
+
"min_verdict_score": min_verdict_score,
|
|
291
|
+
}
|
|
292
|
+
result = await self._request("POST", "/api/retrieve/code-blocks", json=data)
|
|
293
|
+
|
|
294
|
+
code_blocks = [
|
|
295
|
+
CodeBlockMatch(
|
|
296
|
+
code_block=CodeBlock(**cb["code_block"]),
|
|
297
|
+
score=cb["score"],
|
|
298
|
+
verdict_score=cb["verdict_score"],
|
|
299
|
+
thumbs_up=cb["thumbs_up"],
|
|
300
|
+
thumbs_down=cb["thumbs_down"],
|
|
301
|
+
recent_executions=cb.get("recent_executions", []),
|
|
302
|
+
)
|
|
303
|
+
for cb in result["code_blocks"]
|
|
304
|
+
]
|
|
305
|
+
return RetrieveCodeBlockResponse(
|
|
306
|
+
code_blocks=code_blocks,
|
|
307
|
+
total_found=result["total_found"],
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
async def retrieve_best(
|
|
311
|
+
self,
|
|
312
|
+
task: str,
|
|
313
|
+
top_k: int = 10,
|
|
314
|
+
min_verdict_score: float = 0.0,
|
|
315
|
+
) -> RetrieveBestResponse:
|
|
316
|
+
"""Get the single best code block for a task using verdict-aware scoring"""
|
|
317
|
+
data = {
|
|
318
|
+
"task": task,
|
|
319
|
+
"top_k": top_k,
|
|
320
|
+
"min_verdict_score": min_verdict_score,
|
|
321
|
+
}
|
|
322
|
+
result = await self._request("POST", "/api/retrieve/best-for-task", json=data)
|
|
323
|
+
|
|
324
|
+
best_match = None
|
|
325
|
+
if result.get("best_match"):
|
|
326
|
+
bm = result["best_match"]
|
|
327
|
+
best_match = BestMatch(
|
|
328
|
+
code_block=CodeBlock(**bm["code_block"]),
|
|
329
|
+
combined_score=bm["combined_score"],
|
|
330
|
+
vector_score=bm["vector_score"],
|
|
331
|
+
verdict_score=bm["verdict_score"],
|
|
332
|
+
error_resilience=bm["error_resilience"],
|
|
333
|
+
thumbs_up=bm["thumbs_up"],
|
|
334
|
+
thumbs_down=bm["thumbs_down"],
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
alternatives = [AlternativeCandidate(**alt) for alt in result.get("alternative_candidates", [])]
|
|
338
|
+
|
|
339
|
+
return RetrieveBestResponse(
|
|
340
|
+
best_match=best_match,
|
|
341
|
+
alternative_candidates=alternatives,
|
|
342
|
+
retrieval_confidence=result["retrieval_confidence"],
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
async def get_few_shot_examples(
|
|
346
|
+
self,
|
|
347
|
+
task: str,
|
|
348
|
+
k: int = 3,
|
|
349
|
+
) -> list[FewShotExample]:
|
|
350
|
+
"""Retrieve few-shot examples for code generation"""
|
|
351
|
+
data = {"task": task, "k": k}
|
|
352
|
+
result = await self._request("POST", "/api/retrieve/few-shot-examples", json=data)
|
|
353
|
+
return [FewShotExample(**ex) for ex in result["examples"]]
|
|
354
|
+
|
|
355
|
+
async def get_task_patterns(
|
|
356
|
+
self,
|
|
357
|
+
task: str | None = None,
|
|
358
|
+
code_block_id: str | None = None,
|
|
359
|
+
min_thumbs_up: int = 0,
|
|
360
|
+
top_k: int = 20,
|
|
361
|
+
) -> list[TaskPattern]:
|
|
362
|
+
"""Retrieve proven task->code mappings"""
|
|
363
|
+
data = {
|
|
364
|
+
"task": task,
|
|
365
|
+
"code_block_id": code_block_id,
|
|
366
|
+
"min_thumbs_up": min_thumbs_up,
|
|
367
|
+
"top_k": top_k,
|
|
368
|
+
}
|
|
369
|
+
result = await self._request("POST", "/api/retrieve/task-patterns", json=data)
|
|
370
|
+
return [TaskPattern(**p) for p in result["patterns"]]
|
|
371
|
+
|
|
372
|
+
async def get_code_files(
|
|
373
|
+
self,
|
|
374
|
+
task: str,
|
|
375
|
+
top_k: int = 5,
|
|
376
|
+
min_verdict_score: float = 0.3,
|
|
377
|
+
prefer_complete: bool = True,
|
|
378
|
+
cache_dir: str = ".raysurfer_code",
|
|
379
|
+
) -> GetCodeFilesResponse:
|
|
380
|
+
"""
|
|
381
|
+
Get code files for a task, ready to download to sandbox.
|
|
382
|
+
|
|
383
|
+
Returns code blocks with full source code, optimized for:
|
|
384
|
+
- High verdict scores (proven to work)
|
|
385
|
+
- More complete implementations (prefer longer source)
|
|
386
|
+
- Task relevance (semantic similarity)
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
task: Task description for semantic search
|
|
390
|
+
top_k: Maximum number of files to return
|
|
391
|
+
min_verdict_score: Minimum quality score (0-1)
|
|
392
|
+
prefer_complete: Whether to prefer longer/more complete implementations
|
|
393
|
+
cache_dir: Directory path where files will be written (default: .raysurfer_code).
|
|
394
|
+
Used to generate full paths in add_to_llm_prompt.
|
|
395
|
+
"""
|
|
396
|
+
data = {
|
|
397
|
+
"task": task,
|
|
398
|
+
"top_k": top_k,
|
|
399
|
+
"min_verdict_score": min_verdict_score,
|
|
400
|
+
"prefer_complete": prefer_complete,
|
|
401
|
+
}
|
|
402
|
+
result = await self._request("POST", "/api/retrieve/code-files", json=data)
|
|
403
|
+
files = [CodeFile(**f) for f in result["files"]]
|
|
404
|
+
|
|
405
|
+
# Generate the add_to_llm_prompt string
|
|
406
|
+
add_to_llm_prompt = self._format_llm_prompt(files, cache_dir)
|
|
407
|
+
|
|
408
|
+
return GetCodeFilesResponse(
|
|
409
|
+
files=files,
|
|
410
|
+
task=result["task"],
|
|
411
|
+
total_found=result["total_found"],
|
|
412
|
+
add_to_llm_prompt=add_to_llm_prompt,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def _format_llm_prompt(self, files: list[CodeFile], cache_dir: str | None = None) -> str:
|
|
416
|
+
"""Format a prompt string listing all retrieved code files."""
|
|
417
|
+
if not files:
|
|
418
|
+
return ""
|
|
419
|
+
|
|
420
|
+
lines = [
|
|
421
|
+
"\n\n## IMPORTANT: Pre-validated Code Files Available\n",
|
|
422
|
+
"The following validated code has been retrieved from the cache. "
|
|
423
|
+
"Use these files directly instead of regenerating code.\n",
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
for f in files:
|
|
427
|
+
if cache_dir:
|
|
428
|
+
import os
|
|
429
|
+
|
|
430
|
+
full_path = os.path.join(cache_dir, f.filename)
|
|
431
|
+
lines.append(f"\n### `{f.filename}` -> `{full_path}`")
|
|
432
|
+
else:
|
|
433
|
+
lines.append(f"\n### `{f.filename}`")
|
|
434
|
+
lines.append(f"- **Description**: {f.description}")
|
|
435
|
+
lines.append(f"- **Language**: {f.language}")
|
|
436
|
+
lines.append(f"- **Entrypoint**: `{f.entrypoint}`")
|
|
437
|
+
lines.append(f"- **Confidence**: {f.verdict_score:.0%}")
|
|
438
|
+
if f.dependencies:
|
|
439
|
+
lines.append(f"- **Dependencies**: {', '.join(f.dependencies)}")
|
|
440
|
+
|
|
441
|
+
lines.append("\n\n**Instructions**:")
|
|
442
|
+
lines.append("1. Read the cached file(s) before writing new code")
|
|
443
|
+
lines.append("2. Use the cached code as your starting point")
|
|
444
|
+
lines.append("3. Only modify if the task requires specific changes")
|
|
445
|
+
lines.append("4. Do not regenerate code that already exists\n")
|
|
446
|
+
|
|
447
|
+
return "\n".join(lines)
|
|
448
|
+
|
|
449
|
+
async def vote_code_snip(
|
|
450
|
+
self,
|
|
451
|
+
task: str,
|
|
452
|
+
code_block_id: str,
|
|
453
|
+
code_block_name: str,
|
|
454
|
+
code_block_description: str,
|
|
455
|
+
succeeded: bool,
|
|
456
|
+
) -> dict[str, Any]:
|
|
457
|
+
"""
|
|
458
|
+
Vote on whether a cached code snippet was useful.
|
|
459
|
+
|
|
460
|
+
This triggers background voting to assess whether the cached code
|
|
461
|
+
actually helped complete the task successfully.
|
|
462
|
+
"""
|
|
463
|
+
data = {
|
|
464
|
+
"task": task,
|
|
465
|
+
"code_block_id": code_block_id,
|
|
466
|
+
"code_block_name": code_block_name,
|
|
467
|
+
"code_block_description": code_block_description,
|
|
468
|
+
"succeeded": succeeded,
|
|
469
|
+
}
|
|
470
|
+
return await self._request("POST", "/api/store/cache-usage", json=data)
|
|
471
|
+
|
|
472
|
+
# =========================================================================
|
|
473
|
+
# Auto Review API
|
|
474
|
+
# =========================================================================
|
|
475
|
+
|
|
476
|
+
async def auto_review(
|
|
477
|
+
self,
|
|
478
|
+
execution_id: str,
|
|
479
|
+
triggering_task: str,
|
|
480
|
+
execution_state: ExecutionState,
|
|
481
|
+
input_data: dict[str, Any],
|
|
482
|
+
output_data: Any,
|
|
483
|
+
code_block_name: str,
|
|
484
|
+
code_block_description: str,
|
|
485
|
+
error_message: str | None = None,
|
|
486
|
+
) -> AutoReviewResponse:
|
|
487
|
+
"""
|
|
488
|
+
Get an auto-generated review using Claude Opus 4.5.
|
|
489
|
+
Useful for programmatically reviewing execution results.
|
|
490
|
+
"""
|
|
491
|
+
data = {
|
|
492
|
+
"execution_id": execution_id,
|
|
493
|
+
"triggering_task": triggering_task,
|
|
494
|
+
"execution_state": execution_state.value,
|
|
495
|
+
"input_data": input_data,
|
|
496
|
+
"output_data": output_data,
|
|
497
|
+
"code_block_name": code_block_name,
|
|
498
|
+
"code_block_description": code_block_description,
|
|
499
|
+
"error_message": error_message,
|
|
500
|
+
}
|
|
501
|
+
result = await self._request("POST", "/api/store/auto-review", json=data)
|
|
502
|
+
return AutoReviewResponse(
|
|
503
|
+
success=result["success"],
|
|
504
|
+
execution_id=result["execution_id"],
|
|
505
|
+
review=AgentReview(**result["review"]),
|
|
506
|
+
message=result["message"],
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
async def get_executions(
|
|
510
|
+
self,
|
|
511
|
+
code_block_id: str | None = None,
|
|
512
|
+
task: str | None = None,
|
|
513
|
+
verdict: AgentVerdict | None = None,
|
|
514
|
+
limit: int = 20,
|
|
515
|
+
) -> RetrieveExecutionsResponse:
|
|
516
|
+
"""Retrieve execution records by code block ID, task, or verdict."""
|
|
517
|
+
data = {
|
|
518
|
+
"code_block_id": code_block_id,
|
|
519
|
+
"task": task,
|
|
520
|
+
"verdict": verdict.value if verdict else None,
|
|
521
|
+
"limit": limit,
|
|
522
|
+
}
|
|
523
|
+
result = await self._request("POST", "/api/retrieve/executions", json=data)
|
|
524
|
+
executions = [ExecutionRecord(**ex) for ex in result["executions"]]
|
|
525
|
+
return RetrieveExecutionsResponse(
|
|
526
|
+
executions=executions,
|
|
527
|
+
total_found=result["total_found"],
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class RaySurfer:
|
|
532
|
+
"""Sync client for RaySurfer API"""
|
|
533
|
+
|
|
534
|
+
def __init__(
|
|
535
|
+
self,
|
|
536
|
+
api_key: str | None = None,
|
|
537
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
538
|
+
timeout: float = 60.0,
|
|
539
|
+
organization_id: str | None = None,
|
|
540
|
+
workspace_id: str | None = None,
|
|
541
|
+
snips_desired: SnipsDesired | str | None = None,
|
|
542
|
+
namespace: str | None = None,
|
|
543
|
+
):
|
|
544
|
+
"""
|
|
545
|
+
Initialize the RaySurfer sync client.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
api_key: RaySurfer API key (or set RAYSURFER_API_KEY env var)
|
|
549
|
+
base_url: API base URL
|
|
550
|
+
timeout: Request timeout in seconds
|
|
551
|
+
organization_id: Optional organization ID for dedicated namespace (team/enterprise)
|
|
552
|
+
workspace_id: Optional workspace ID for client-specific namespace (enterprise only)
|
|
553
|
+
snips_desired: Scope of private snippets - "company" (Team/Enterprise) or "client" (Enterprise only)
|
|
554
|
+
namespace: Custom namespace for code storage/retrieval (overrides org-based namespacing)
|
|
555
|
+
"""
|
|
556
|
+
self.api_key = api_key
|
|
557
|
+
self.base_url = base_url.rstrip("/")
|
|
558
|
+
self.timeout = timeout
|
|
559
|
+
self.organization_id = organization_id
|
|
560
|
+
self.workspace_id = workspace_id
|
|
561
|
+
self.namespace = namespace
|
|
562
|
+
# Convert string to SnipsDesired if needed
|
|
563
|
+
if isinstance(snips_desired, str):
|
|
564
|
+
self.snips_desired = SnipsDesired(snips_desired) if snips_desired else None
|
|
565
|
+
else:
|
|
566
|
+
self.snips_desired = snips_desired
|
|
567
|
+
self._client: httpx.Client | None = None
|
|
568
|
+
|
|
569
|
+
def _get_client(self) -> httpx.Client:
|
|
570
|
+
if self._client is None:
|
|
571
|
+
headers = {"Content-Type": "application/json"}
|
|
572
|
+
if self.api_key:
|
|
573
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
574
|
+
# Add organization/workspace headers for namespace routing
|
|
575
|
+
if self.organization_id:
|
|
576
|
+
headers["X-Raysurfer-Org-Id"] = self.organization_id
|
|
577
|
+
if self.workspace_id:
|
|
578
|
+
headers["X-Raysurfer-Workspace-Id"] = self.workspace_id
|
|
579
|
+
# Add snippet retrieval scope headers
|
|
580
|
+
if self.snips_desired:
|
|
581
|
+
headers["X-Raysurfer-Snips-Desired"] = self.snips_desired.value
|
|
582
|
+
# Custom namespace override
|
|
583
|
+
if self.namespace:
|
|
584
|
+
headers["X-Raysurfer-Namespace"] = self.namespace
|
|
585
|
+
# SDK version for tracking
|
|
586
|
+
headers["X-Raysurfer-SDK-Version"] = f"python/{__version__}"
|
|
587
|
+
self._client = httpx.Client(
|
|
588
|
+
base_url=self.base_url,
|
|
589
|
+
headers=headers,
|
|
590
|
+
timeout=self.timeout,
|
|
591
|
+
)
|
|
592
|
+
return self._client
|
|
593
|
+
|
|
594
|
+
def close(self) -> None:
|
|
595
|
+
if self._client:
|
|
596
|
+
self._client.close()
|
|
597
|
+
self._client = None
|
|
598
|
+
|
|
599
|
+
def __enter__(self) -> "RaySurfer":
|
|
600
|
+
return self
|
|
601
|
+
|
|
602
|
+
def __exit__(self, *args: Any) -> None:
|
|
603
|
+
self.close()
|
|
604
|
+
|
|
605
|
+
def _request(self, method: str, path: str, **kwargs: Any) -> dict[str, Any]:
|
|
606
|
+
import time as _time
|
|
607
|
+
|
|
608
|
+
client = self._get_client()
|
|
609
|
+
last_exception: Exception | None = None
|
|
610
|
+
|
|
611
|
+
for attempt in range(MAX_RETRIES):
|
|
612
|
+
try:
|
|
613
|
+
response = client.request(method, path, **kwargs)
|
|
614
|
+
|
|
615
|
+
if response.status_code == 401:
|
|
616
|
+
raise AuthenticationError("Invalid API key")
|
|
617
|
+
if response.status_code == 429:
|
|
618
|
+
retry_after = response.headers.get("Retry-After")
|
|
619
|
+
delay = float(retry_after) if retry_after else RETRY_BASE_DELAY * (2**attempt)
|
|
620
|
+
if attempt < MAX_RETRIES - 1:
|
|
621
|
+
logger.warning(f"Rate limited, retrying in {delay:.1f}s (attempt {attempt + 1}/{MAX_RETRIES})")
|
|
622
|
+
_time.sleep(delay)
|
|
623
|
+
continue
|
|
624
|
+
raise RateLimitError(retry_after=delay)
|
|
625
|
+
if response.status_code in RETRYABLE_STATUS_CODES:
|
|
626
|
+
if attempt < MAX_RETRIES - 1:
|
|
627
|
+
delay = RETRY_BASE_DELAY * (2**attempt)
|
|
628
|
+
logger.warning(
|
|
629
|
+
f"Server error {response.status_code}, retrying in {delay:.1f}s (attempt {attempt + 1}/{MAX_RETRIES})"
|
|
630
|
+
)
|
|
631
|
+
_time.sleep(delay)
|
|
632
|
+
continue
|
|
633
|
+
if response.status_code >= 400:
|
|
634
|
+
raise APIError(response.text, status_code=response.status_code)
|
|
635
|
+
|
|
636
|
+
return response.json()
|
|
637
|
+
except (httpx.ConnectError, httpx.TimeoutException) as e:
|
|
638
|
+
last_exception = e
|
|
639
|
+
if attempt < MAX_RETRIES - 1:
|
|
640
|
+
delay = RETRY_BASE_DELAY * (2**attempt)
|
|
641
|
+
logger.warning(
|
|
642
|
+
f"Network error: {e}, retrying in {delay:.1f}s (attempt {attempt + 1}/{MAX_RETRIES})"
|
|
643
|
+
)
|
|
644
|
+
_time.sleep(delay)
|
|
645
|
+
continue
|
|
646
|
+
raise CacheUnavailableError(f"Failed to connect after {MAX_RETRIES} attempts: {e}") from e
|
|
647
|
+
|
|
648
|
+
# Should not reach here, but just in case
|
|
649
|
+
raise CacheUnavailableError(f"Request failed after {MAX_RETRIES} attempts") from last_exception
|
|
650
|
+
|
|
651
|
+
# =========================================================================
|
|
652
|
+
# Store API
|
|
653
|
+
# =========================================================================
|
|
654
|
+
|
|
655
|
+
def store_code_block(
|
|
656
|
+
self,
|
|
657
|
+
name: str,
|
|
658
|
+
source: str,
|
|
659
|
+
entrypoint: str,
|
|
660
|
+
language: str,
|
|
661
|
+
description: str = "",
|
|
662
|
+
input_schema: dict[str, Any] | None = None,
|
|
663
|
+
output_schema: dict[str, Any] | None = None,
|
|
664
|
+
language_version: str | None = None,
|
|
665
|
+
dependencies: list[str] | None = None,
|
|
666
|
+
tags: list[str] | None = None,
|
|
667
|
+
capabilities: list[str] | None = None,
|
|
668
|
+
example_queries: list[str] | None = None,
|
|
669
|
+
) -> StoreCodeBlockResponse:
|
|
670
|
+
"""Store a new code block"""
|
|
671
|
+
data = {
|
|
672
|
+
"name": name,
|
|
673
|
+
"description": description,
|
|
674
|
+
"source": source,
|
|
675
|
+
"entrypoint": entrypoint,
|
|
676
|
+
"language": language,
|
|
677
|
+
"input_schema": input_schema or {},
|
|
678
|
+
"output_schema": output_schema or {},
|
|
679
|
+
"language_version": language_version,
|
|
680
|
+
"dependencies": dependencies or [],
|
|
681
|
+
"tags": tags or [],
|
|
682
|
+
"capabilities": capabilities or [],
|
|
683
|
+
"example_queries": example_queries,
|
|
684
|
+
}
|
|
685
|
+
result = self._request("POST", "/api/store/code-block", json=data)
|
|
686
|
+
return StoreCodeBlockResponse(**result)
|
|
687
|
+
|
|
688
|
+
def store_execution(
|
|
689
|
+
self,
|
|
690
|
+
code_block_id: str,
|
|
691
|
+
triggering_task: str,
|
|
692
|
+
input_data: dict[str, Any],
|
|
693
|
+
output_data: Any,
|
|
694
|
+
execution_state: ExecutionState = ExecutionState.COMPLETED,
|
|
695
|
+
duration_ms: int = 0,
|
|
696
|
+
error_message: str | None = None,
|
|
697
|
+
error_type: str | None = None,
|
|
698
|
+
verdict: AgentVerdict | None = None,
|
|
699
|
+
review: AgentReview | None = None,
|
|
700
|
+
) -> StoreExecutionResponse:
|
|
701
|
+
"""Store an execution record"""
|
|
702
|
+
io = ExecutionIO(
|
|
703
|
+
input_data=input_data,
|
|
704
|
+
output_data=output_data,
|
|
705
|
+
output_type=type(output_data).__name__,
|
|
706
|
+
)
|
|
707
|
+
data = {
|
|
708
|
+
"code_block_id": code_block_id,
|
|
709
|
+
"triggering_task": triggering_task,
|
|
710
|
+
"io": io.model_dump(),
|
|
711
|
+
"execution_state": execution_state.value,
|
|
712
|
+
"duration_ms": duration_ms,
|
|
713
|
+
"error_message": error_message,
|
|
714
|
+
"error_type": error_type,
|
|
715
|
+
"verdict": verdict.value if verdict else None,
|
|
716
|
+
"review": review.model_dump() if review else None,
|
|
717
|
+
}
|
|
718
|
+
result = self._request("POST", "/api/store/execution", json=data)
|
|
719
|
+
return StoreExecutionResponse(**result)
|
|
720
|
+
|
|
721
|
+
def upload_new_code_snips(
|
|
722
|
+
self,
|
|
723
|
+
task: str,
|
|
724
|
+
files_written: list[FileWritten],
|
|
725
|
+
succeeded: bool,
|
|
726
|
+
auto_vote: bool = True,
|
|
727
|
+
execution_logs: str | None = None,
|
|
728
|
+
) -> SubmitExecutionResultResponse:
|
|
729
|
+
"""
|
|
730
|
+
Upload new code snippets from an execution.
|
|
731
|
+
|
|
732
|
+
This is the simplified API for agent integrations. Just send:
|
|
733
|
+
- The task that was executed
|
|
734
|
+
- Files that were written during execution
|
|
735
|
+
- Whether the task succeeded
|
|
736
|
+
- Whether to auto-vote on stored blocks (default: True)
|
|
737
|
+
- Captured execution logs for vote context
|
|
738
|
+
|
|
739
|
+
Backend handles: entrypoint detection, tag extraction, language detection,
|
|
740
|
+
deduplication, quality checks, and storage.
|
|
741
|
+
"""
|
|
742
|
+
data: dict[str, Any] = {
|
|
743
|
+
"task": task,
|
|
744
|
+
"files_written": [f.model_dump() for f in files_written],
|
|
745
|
+
"succeeded": succeeded,
|
|
746
|
+
"auto_vote": auto_vote,
|
|
747
|
+
}
|
|
748
|
+
if execution_logs is not None:
|
|
749
|
+
data["execution_logs"] = execution_logs
|
|
750
|
+
result = self._request("POST", "/api/store/execution-result", json=data)
|
|
751
|
+
return SubmitExecutionResultResponse(**result)
|
|
752
|
+
|
|
753
|
+
# =========================================================================
|
|
754
|
+
# Retrieve API
|
|
755
|
+
# =========================================================================
|
|
756
|
+
|
|
757
|
+
def get_code_snips(
|
|
758
|
+
self,
|
|
759
|
+
task: str,
|
|
760
|
+
top_k: int = 10,
|
|
761
|
+
min_verdict_score: float = 0.0,
|
|
762
|
+
) -> RetrieveCodeBlockResponse:
|
|
763
|
+
"""
|
|
764
|
+
Get cached code snippets for a task.
|
|
765
|
+
|
|
766
|
+
Searches for code blocks by task description using semantic search.
|
|
767
|
+
"""
|
|
768
|
+
data = {
|
|
769
|
+
"task": task,
|
|
770
|
+
"top_k": top_k,
|
|
771
|
+
"min_verdict_score": min_verdict_score,
|
|
772
|
+
}
|
|
773
|
+
result = self._request("POST", "/api/retrieve/code-blocks", json=data)
|
|
774
|
+
|
|
775
|
+
code_blocks = [
|
|
776
|
+
CodeBlockMatch(
|
|
777
|
+
code_block=CodeBlock(**cb["code_block"]),
|
|
778
|
+
score=cb["score"],
|
|
779
|
+
verdict_score=cb["verdict_score"],
|
|
780
|
+
thumbs_up=cb["thumbs_up"],
|
|
781
|
+
thumbs_down=cb["thumbs_down"],
|
|
782
|
+
recent_executions=cb.get("recent_executions", []),
|
|
783
|
+
)
|
|
784
|
+
for cb in result["code_blocks"]
|
|
785
|
+
]
|
|
786
|
+
return RetrieveCodeBlockResponse(
|
|
787
|
+
code_blocks=code_blocks,
|
|
788
|
+
total_found=result["total_found"],
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
def retrieve_best(
|
|
792
|
+
self,
|
|
793
|
+
task: str,
|
|
794
|
+
top_k: int = 10,
|
|
795
|
+
min_verdict_score: float = 0.0,
|
|
796
|
+
) -> RetrieveBestResponse:
|
|
797
|
+
"""Get the single best code block for a task using verdict-aware scoring"""
|
|
798
|
+
data = {
|
|
799
|
+
"task": task,
|
|
800
|
+
"top_k": top_k,
|
|
801
|
+
"min_verdict_score": min_verdict_score,
|
|
802
|
+
}
|
|
803
|
+
result = self._request("POST", "/api/retrieve/best-for-task", json=data)
|
|
804
|
+
|
|
805
|
+
best_match = None
|
|
806
|
+
if result.get("best_match"):
|
|
807
|
+
bm = result["best_match"]
|
|
808
|
+
best_match = BestMatch(
|
|
809
|
+
code_block=CodeBlock(**bm["code_block"]),
|
|
810
|
+
combined_score=bm["combined_score"],
|
|
811
|
+
vector_score=bm["vector_score"],
|
|
812
|
+
verdict_score=bm["verdict_score"],
|
|
813
|
+
error_resilience=bm["error_resilience"],
|
|
814
|
+
thumbs_up=bm["thumbs_up"],
|
|
815
|
+
thumbs_down=bm["thumbs_down"],
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
alternatives = [AlternativeCandidate(**alt) for alt in result.get("alternative_candidates", [])]
|
|
819
|
+
|
|
820
|
+
return RetrieveBestResponse(
|
|
821
|
+
best_match=best_match,
|
|
822
|
+
alternative_candidates=alternatives,
|
|
823
|
+
retrieval_confidence=result["retrieval_confidence"],
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
def get_few_shot_examples(
|
|
827
|
+
self,
|
|
828
|
+
task: str,
|
|
829
|
+
k: int = 3,
|
|
830
|
+
) -> list[FewShotExample]:
|
|
831
|
+
"""Retrieve few-shot examples for code generation"""
|
|
832
|
+
data = {"task": task, "k": k}
|
|
833
|
+
result = self._request("POST", "/api/retrieve/few-shot-examples", json=data)
|
|
834
|
+
return [FewShotExample(**ex) for ex in result["examples"]]
|
|
835
|
+
|
|
836
|
+
def get_task_patterns(
|
|
837
|
+
self,
|
|
838
|
+
task: str | None = None,
|
|
839
|
+
code_block_id: str | None = None,
|
|
840
|
+
min_thumbs_up: int = 0,
|
|
841
|
+
top_k: int = 20,
|
|
842
|
+
) -> list[TaskPattern]:
|
|
843
|
+
"""Retrieve proven task->code mappings"""
|
|
844
|
+
data = {
|
|
845
|
+
"task": task,
|
|
846
|
+
"code_block_id": code_block_id,
|
|
847
|
+
"min_thumbs_up": min_thumbs_up,
|
|
848
|
+
"top_k": top_k,
|
|
849
|
+
}
|
|
850
|
+
result = self._request("POST", "/api/retrieve/task-patterns", json=data)
|
|
851
|
+
return [TaskPattern(**p) for p in result["patterns"]]
|
|
852
|
+
|
|
853
|
+
def get_code_files(
|
|
854
|
+
self,
|
|
855
|
+
task: str,
|
|
856
|
+
top_k: int = 5,
|
|
857
|
+
min_verdict_score: float = 0.3,
|
|
858
|
+
prefer_complete: bool = True,
|
|
859
|
+
cache_dir: str = ".raysurfer_code",
|
|
860
|
+
) -> GetCodeFilesResponse:
|
|
861
|
+
"""
|
|
862
|
+
Get code files for a task, ready to download to sandbox.
|
|
863
|
+
|
|
864
|
+
Returns code blocks with full source code, optimized for:
|
|
865
|
+
- High verdict scores (proven to work)
|
|
866
|
+
- More complete implementations (prefer longer source)
|
|
867
|
+
- Task relevance (semantic similarity)
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
task: Task description for semantic search
|
|
871
|
+
top_k: Maximum number of files to return
|
|
872
|
+
min_verdict_score: Minimum quality score (0-1)
|
|
873
|
+
prefer_complete: Whether to prefer longer/more complete implementations
|
|
874
|
+
cache_dir: Directory path where files will be written (default: .raysurfer_code).
|
|
875
|
+
Used to generate full paths in add_to_llm_prompt.
|
|
876
|
+
"""
|
|
877
|
+
data = {
|
|
878
|
+
"task": task,
|
|
879
|
+
"top_k": top_k,
|
|
880
|
+
"min_verdict_score": min_verdict_score,
|
|
881
|
+
"prefer_complete": prefer_complete,
|
|
882
|
+
}
|
|
883
|
+
result = self._request("POST", "/api/retrieve/code-files", json=data)
|
|
884
|
+
files = [CodeFile(**f) for f in result["files"]]
|
|
885
|
+
|
|
886
|
+
# Generate the add_to_llm_prompt string
|
|
887
|
+
add_to_llm_prompt = self._format_llm_prompt(files, cache_dir)
|
|
888
|
+
|
|
889
|
+
return GetCodeFilesResponse(
|
|
890
|
+
files=files,
|
|
891
|
+
task=result["task"],
|
|
892
|
+
total_found=result["total_found"],
|
|
893
|
+
add_to_llm_prompt=add_to_llm_prompt,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
def _format_llm_prompt(self, files: list[CodeFile], cache_dir: str | None = None) -> str:
|
|
897
|
+
"""Format a prompt string listing all retrieved code files."""
|
|
898
|
+
if not files:
|
|
899
|
+
return ""
|
|
900
|
+
|
|
901
|
+
lines = [
|
|
902
|
+
"\n\n## IMPORTANT: Pre-validated Code Files Available\n",
|
|
903
|
+
"The following validated code has been retrieved from the cache. "
|
|
904
|
+
"Use these files directly instead of regenerating code.\n",
|
|
905
|
+
]
|
|
906
|
+
|
|
907
|
+
for f in files:
|
|
908
|
+
if cache_dir:
|
|
909
|
+
import os
|
|
910
|
+
|
|
911
|
+
full_path = os.path.join(cache_dir, f.filename)
|
|
912
|
+
lines.append(f"\n### `{f.filename}` -> `{full_path}`")
|
|
913
|
+
else:
|
|
914
|
+
lines.append(f"\n### `{f.filename}`")
|
|
915
|
+
lines.append(f"- **Description**: {f.description}")
|
|
916
|
+
lines.append(f"- **Language**: {f.language}")
|
|
917
|
+
lines.append(f"- **Entrypoint**: `{f.entrypoint}`")
|
|
918
|
+
lines.append(f"- **Confidence**: {f.verdict_score:.0%}")
|
|
919
|
+
if f.dependencies:
|
|
920
|
+
lines.append(f"- **Dependencies**: {', '.join(f.dependencies)}")
|
|
921
|
+
|
|
922
|
+
lines.append("\n\n**Instructions**:")
|
|
923
|
+
lines.append("1. Read the cached file(s) before writing new code")
|
|
924
|
+
lines.append("2. Use the cached code as your starting point")
|
|
925
|
+
lines.append("3. Only modify if the task requires specific changes")
|
|
926
|
+
lines.append("4. Do not regenerate code that already exists\n")
|
|
927
|
+
|
|
928
|
+
return "\n".join(lines)
|
|
929
|
+
|
|
930
|
+
def vote_code_snip(
|
|
931
|
+
self,
|
|
932
|
+
task: str,
|
|
933
|
+
code_block_id: str,
|
|
934
|
+
code_block_name: str,
|
|
935
|
+
code_block_description: str,
|
|
936
|
+
succeeded: bool,
|
|
937
|
+
) -> dict[str, Any]:
|
|
938
|
+
"""
|
|
939
|
+
Vote on whether a cached code snippet was useful.
|
|
940
|
+
|
|
941
|
+
This triggers background voting to assess whether the cached code
|
|
942
|
+
actually helped complete the task successfully.
|
|
943
|
+
"""
|
|
944
|
+
data = {
|
|
945
|
+
"task": task,
|
|
946
|
+
"code_block_id": code_block_id,
|
|
947
|
+
"code_block_name": code_block_name,
|
|
948
|
+
"code_block_description": code_block_description,
|
|
949
|
+
"succeeded": succeeded,
|
|
950
|
+
}
|
|
951
|
+
return self._request("POST", "/api/store/cache-usage", json=data)
|
|
952
|
+
|
|
953
|
+
# =========================================================================
|
|
954
|
+
# Auto Review API
|
|
955
|
+
# =========================================================================
|
|
956
|
+
|
|
957
|
+
def auto_review(
|
|
958
|
+
self,
|
|
959
|
+
execution_id: str,
|
|
960
|
+
triggering_task: str,
|
|
961
|
+
execution_state: ExecutionState,
|
|
962
|
+
input_data: dict[str, Any],
|
|
963
|
+
output_data: Any,
|
|
964
|
+
code_block_name: str,
|
|
965
|
+
code_block_description: str,
|
|
966
|
+
error_message: str | None = None,
|
|
967
|
+
) -> AutoReviewResponse:
|
|
968
|
+
"""
|
|
969
|
+
Get an auto-generated review using Claude Opus 4.5.
|
|
970
|
+
Useful for programmatically reviewing execution results.
|
|
971
|
+
"""
|
|
972
|
+
data = {
|
|
973
|
+
"execution_id": execution_id,
|
|
974
|
+
"triggering_task": triggering_task,
|
|
975
|
+
"execution_state": execution_state.value,
|
|
976
|
+
"input_data": input_data,
|
|
977
|
+
"output_data": output_data,
|
|
978
|
+
"code_block_name": code_block_name,
|
|
979
|
+
"code_block_description": code_block_description,
|
|
980
|
+
"error_message": error_message,
|
|
981
|
+
}
|
|
982
|
+
result = self._request("POST", "/api/store/auto-review", json=data)
|
|
983
|
+
return AutoReviewResponse(
|
|
984
|
+
success=result["success"],
|
|
985
|
+
execution_id=result["execution_id"],
|
|
986
|
+
review=AgentReview(**result["review"]),
|
|
987
|
+
message=result["message"],
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
def get_executions(
|
|
991
|
+
self,
|
|
992
|
+
code_block_id: str | None = None,
|
|
993
|
+
task: str | None = None,
|
|
994
|
+
verdict: AgentVerdict | None = None,
|
|
995
|
+
limit: int = 20,
|
|
996
|
+
) -> RetrieveExecutionsResponse:
|
|
997
|
+
"""Retrieve execution records by code block ID, task, or verdict."""
|
|
998
|
+
data = {
|
|
999
|
+
"code_block_id": code_block_id,
|
|
1000
|
+
"task": task,
|
|
1001
|
+
"verdict": verdict.value if verdict else None,
|
|
1002
|
+
"limit": limit,
|
|
1003
|
+
}
|
|
1004
|
+
result = self._request("POST", "/api/retrieve/executions", json=data)
|
|
1005
|
+
executions = [ExecutionRecord(**ex) for ex in result["executions"]]
|
|
1006
|
+
return RetrieveExecutionsResponse(
|
|
1007
|
+
executions=executions,
|
|
1008
|
+
total_found=result["total_found"],
|
|
1009
|
+
)
|