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.
@@ -0,0 +1,21 @@
1
+ """RaySurfer SDK exceptions"""
2
+
3
+
4
+ class RaySurferError(Exception):
5
+ """Base exception for RaySurfer SDK"""
6
+
7
+ pass
8
+
9
+
10
+ class APIError(RaySurferError):
11
+ """API returned an error response"""
12
+
13
+ def __init__(self, message: str, status_code: int | None = None):
14
+ super().__init__(message)
15
+ self.status_code = status_code
16
+
17
+
18
+ class AuthenticationError(RaySurferError):
19
+ """Authentication failed"""
20
+
21
+ pass
@@ -0,0 +1,552 @@
1
+ """
2
+ Drop-in replacement for Claude Agent SDK with automatic code caching.
3
+
4
+ Simply swap your import and rename your client:
5
+ # Before
6
+ from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
7
+ client = ClaudeSDKClient(options)
8
+ await client.query("task")
9
+
10
+ # After
11
+ from raysurfer import RaysurferClient
12
+ from claude_agent_sdk import ClaudeAgentOptions
13
+ client = RaysurferClient(options)
14
+ await client.raysurfer_query("task")
15
+
16
+ Options come directly from claude_agent_sdk - no Raysurfer-specific options needed.
17
+ """
18
+
19
+ import atexit
20
+ import logging
21
+ import os
22
+ import re
23
+ import shutil
24
+ import tempfile
25
+ from typing import Any, AsyncIterator
26
+
27
+ # Isolate temp directory to avoid file watcher conflicts when running
28
+ # nested inside another Claude Code session.
29
+ _ISOLATED_TMPDIR: str | None = None
30
+
31
+
32
+ def _setup_isolated_env() -> str:
33
+ """Set up isolated temp directory for nested Claude Code execution."""
34
+ global _ISOLATED_TMPDIR
35
+
36
+ if _ISOLATED_TMPDIR is None:
37
+ _ISOLATED_TMPDIR = tempfile.mkdtemp(prefix="raysurfer_sdk_")
38
+ os.environ["TMPDIR"] = _ISOLATED_TMPDIR
39
+ os.environ["TEMP"] = _ISOLATED_TMPDIR
40
+ os.environ["TMP"] = _ISOLATED_TMPDIR
41
+ os.environ["CLAUDE_AGENT_SDK_SKIP_VERSION_CHECK"] = "1"
42
+ atexit.register(_cleanup_isolated_env)
43
+
44
+ return _ISOLATED_TMPDIR
45
+
46
+
47
+ def _cleanup_isolated_env() -> None:
48
+ """Clean up isolated temp directory on exit."""
49
+ global _ISOLATED_TMPDIR
50
+ if _ISOLATED_TMPDIR and os.path.exists(_ISOLATED_TMPDIR):
51
+ try:
52
+ shutil.rmtree(_ISOLATED_TMPDIR)
53
+ except Exception:
54
+ pass
55
+ _ISOLATED_TMPDIR = None
56
+
57
+
58
+ _setup_isolated_env()
59
+
60
+ # Import from Claude Agent SDK - these are passed through directly
61
+ from claude_agent_sdk import ClaudeAgentOptions
62
+ from claude_agent_sdk import ClaudeSDKClient as _BaseClaudeSDKClient
63
+ from claude_agent_sdk import (
64
+ AgentDefinition,
65
+ AssistantMessage,
66
+ HookMatcher,
67
+ Message,
68
+ ResultMessage,
69
+ SystemMessage,
70
+ TextBlock,
71
+ ThinkingBlock,
72
+ ToolResultBlock,
73
+ ToolUseBlock,
74
+ UserMessage,
75
+ )
76
+
77
+ from raysurfer.client import AsyncRaySurfer
78
+ from raysurfer.types import FileWritten, SnipsDesired
79
+
80
+ logger = logging.getLogger(__name__)
81
+
82
+ # Re-export all SDK types for convenience
83
+ __all__ = [
84
+ "RaysurferClient",
85
+ # Re-exported from Claude Agent SDK (use these directly)
86
+ "ClaudeAgentOptions",
87
+ "AgentDefinition",
88
+ "HookMatcher",
89
+ "Message",
90
+ "UserMessage",
91
+ "AssistantMessage",
92
+ "SystemMessage",
93
+ "ResultMessage",
94
+ "TextBlock",
95
+ "ThinkingBlock",
96
+ "ToolUseBlock",
97
+ "ToolResultBlock",
98
+ ]
99
+
100
+ # Default backend URL
101
+ DEFAULT_RAYSURFER_URL = "https://web-production-3d338.up.railway.app"
102
+
103
+
104
+ class RaysurferClient:
105
+ """
106
+ Drop-in replacement for ClaudeSDKClient with automatic Raysurfer caching.
107
+
108
+ Usage:
109
+ from raysurfer import RaysurferClient
110
+ from claude_agent_sdk import ClaudeAgentOptions
111
+
112
+ options = ClaudeAgentOptions(
113
+ allowed_tools=["Read", "Write", "Bash"],
114
+ system_prompt="You are a helpful assistant.",
115
+ )
116
+
117
+ async with RaysurferClient(options) as client:
118
+ await client.raysurfer_query("Fetch data from GitHub API")
119
+ async for msg in client.raysurfer_response():
120
+ print(msg)
121
+
122
+ Features:
123
+ - Automatic cache retrieval and system prompt augmentation
124
+ - Multi-agent support: subagent prompts are also augmented with cache
125
+ - Bash file tracking: detects files created by Bash commands
126
+ - Hook propagation: user hooks are preserved and work alongside cache hooks
127
+
128
+ Set RAYSURFER_API_KEY environment variable to enable caching.
129
+ Options come directly from claude_agent_sdk.ClaudeAgentOptions.
130
+ """
131
+
132
+ # File extensions we track from Bash output
133
+ TRACKABLE_EXTENSIONS = {
134
+ ".py", ".js", ".ts", ".rb", ".go", ".rs", ".java", ".cpp", ".c", ".h",
135
+ ".pdf", ".docx", ".xlsx", ".csv", ".json", ".yaml", ".yml", ".xml",
136
+ ".html", ".css", ".md", ".txt", ".sh", ".sql"
137
+ }
138
+
139
+ def __init__(
140
+ self,
141
+ options: ClaudeAgentOptions | None = None,
142
+ public_snips: bool = False,
143
+ snips_desired: SnipsDesired | str | None = None,
144
+ ):
145
+ """
146
+ Initialize RaysurferClient.
147
+
148
+ Args:
149
+ options: ClaudeAgentOptions from claude_agent_sdk (passed through directly)
150
+ public_snips: Whether to include public/shared snippets in retrieval (default: False)
151
+ snips_desired: Scope of private snippets - "company" (Team/Enterprise) or "client" (Enterprise only)
152
+ """
153
+ self._options = options or ClaudeAgentOptions()
154
+ self._base_client: _BaseClaudeSDKClient | None = None
155
+ self._raysurfer: AsyncRaySurfer | None = None
156
+ self._current_query: str | None = None
157
+ self._generated_files: list[FileWritten] = []
158
+ self._bash_generated_files: list[str] = []
159
+ self._task_succeeded: bool = False
160
+ self._cache_enabled: bool = False
161
+ self._cached_code_blocks: list[dict[str, Any]] = []
162
+ self._subagent_cache: dict[str, str] = {}
163
+ self._public_snips = public_snips
164
+ self._snips_desired = snips_desired
165
+
166
+ async def __aenter__(self) -> "RaysurferClient":
167
+ """Initialize the client."""
168
+ api_key = os.environ.get("RAYSURFER_API_KEY")
169
+ base_url = os.environ.get("RAYSURFER_BASE_URL", DEFAULT_RAYSURFER_URL)
170
+
171
+ if api_key:
172
+ self._cache_enabled = True
173
+ self._raysurfer = AsyncRaySurfer(
174
+ api_key=api_key,
175
+ base_url=base_url,
176
+ public_snips=self._public_snips,
177
+ snips_desired=self._snips_desired,
178
+ )
179
+ await self._raysurfer.__aenter__()
180
+
181
+ return self
182
+
183
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
184
+ """Clean up resources."""
185
+ if self._base_client:
186
+ await self._base_client.__aexit__(exc_type, exc_val, exc_tb)
187
+ if self._raysurfer:
188
+ await self._raysurfer.__aexit__(exc_type, exc_val, exc_tb)
189
+
190
+ async def raysurfer_query(self, prompt: str) -> None:
191
+ """
192
+ Send a query to Claude with Raysurfer caching.
193
+
194
+ If RAYSURFER_API_KEY is set, automatically retrieves relevant cached
195
+ code and injects it into the system prompt.
196
+
197
+ Args:
198
+ prompt: The task/query to send to Claude
199
+ """
200
+ self._current_query = prompt
201
+ self._generated_files = []
202
+ self._bash_generated_files = []
203
+ self._task_succeeded = False
204
+ self._cached_code_blocks = []
205
+ self._subagent_cache = {}
206
+
207
+ # Pre-fetch cache for subagents if this is a multi-agent system
208
+ if self._options.agents:
209
+ await self._fetch_subagent_cache(self._options.agents)
210
+
211
+ # Retrieve cached code if caching is enabled
212
+ augmented_options = await self._augment_options_with_cache(prompt)
213
+
214
+ # Initialize and query the base client
215
+ self._base_client = _BaseClaudeSDKClient(options=augmented_options)
216
+ await self._base_client.__aenter__()
217
+ await self._base_client.query(prompt)
218
+
219
+ async def raysurfer_response(self) -> AsyncIterator[Message]:
220
+ """
221
+ Receive and yield response messages from Claude.
222
+
223
+ After successful task completion, automatically uploads any
224
+ generated code to the Raysurfer cache.
225
+
226
+ Yields:
227
+ Message objects from Claude
228
+ """
229
+ if not self._base_client:
230
+ raise RuntimeError("Must call raysurfer_query() before raysurfer_response()")
231
+
232
+ last_bash_command: str | None = None
233
+
234
+ async for message in self._base_client.receive_response():
235
+ # Track Write and Bash tool calls
236
+ if isinstance(message, AssistantMessage):
237
+ for block in message.content:
238
+ if isinstance(block, ToolUseBlock):
239
+ if block.name == "Write":
240
+ self._track_file(block.input)
241
+ elif block.name == "Bash":
242
+ last_bash_command = block.input.get("command", "")
243
+ self._track_bash_file_outputs(last_bash_command)
244
+
245
+ # Track files from Bash tool results
246
+ if isinstance(message, AssistantMessage):
247
+ for block in message.content:
248
+ if isinstance(block, ToolResultBlock) and last_bash_command:
249
+ self._extract_files_from_bash_output(
250
+ last_bash_command,
251
+ str(block.content) if hasattr(block, 'content') else ""
252
+ )
253
+ last_bash_command = None
254
+
255
+ # Check for successful completion
256
+ if isinstance(message, ResultMessage):
257
+ if message.subtype == "success":
258
+ self._task_succeeded = True
259
+
260
+ yield message
261
+
262
+ # Upload generated code if task succeeded
263
+ if self._cache_enabled and self._task_succeeded:
264
+ if self._generated_files:
265
+ await self._upload_to_cache()
266
+ await self._cache_bash_generated_files()
267
+
268
+ # Submit votes for cached code blocks that were used
269
+ if self._cache_enabled and self._task_succeeded and self._cached_code_blocks:
270
+ await self._submit_votes()
271
+
272
+ def _track_file(self, tool_input: dict[str, Any]) -> None:
273
+ """Track a file written by the Write tool."""
274
+ file_path = tool_input.get("file_path", "")
275
+ content = tool_input.get("content", "")
276
+ if file_path and content:
277
+ self._generated_files.append(FileWritten(path=file_path, content=content))
278
+
279
+ def _track_bash_file_outputs(self, command: str) -> None:
280
+ """Extract potential output files from Bash commands."""
281
+ patterns = [
282
+ r'>\s*([^\s;&|]+)',
283
+ r'>>\s*([^\s;&|]+)',
284
+ r'-o\s+([^\s;&|]+)',
285
+ r'--output[=\s]+([^\s;&|]+)',
286
+ r'savefig\([\'"]([^\'"]+)[\'"]\)',
287
+ r'to_csv\([\'"]([^\'"]+)[\'"]\)',
288
+ r'to_excel\([\'"]([^\'"]+)[\'"]\)',
289
+ r'write\([\'"]([^\'"]+)[\'"]\)',
290
+ ]
291
+
292
+ for pattern in patterns:
293
+ matches = re.findall(pattern, command)
294
+ for match in matches:
295
+ ext = os.path.splitext(match)[1].lower()
296
+ if ext in self.TRACKABLE_EXTENSIONS:
297
+ self._bash_generated_files.append(match)
298
+
299
+ def _extract_files_from_bash_output(self, command: str, output: str) -> None:
300
+ """Extract files mentioned in Bash command output."""
301
+ file_pattern = r'[/\w.-]+\.\w{2,5}'
302
+ matches = re.findall(file_pattern, output)
303
+ for match in matches:
304
+ ext = os.path.splitext(match)[1].lower()
305
+ if ext in self.TRACKABLE_EXTENSIONS:
306
+ if match not in self._bash_generated_files:
307
+ self._bash_generated_files.append(match)
308
+
309
+ async def _cache_bash_generated_files(self) -> None:
310
+ """Attempt to read and cache files generated by Bash commands."""
311
+ if not self._raysurfer or not self._bash_generated_files:
312
+ return
313
+
314
+ for file_path in self._bash_generated_files:
315
+ try:
316
+ ext = os.path.splitext(file_path)[1].lower()
317
+ if ext in {".py", ".js", ".ts", ".rb", ".go", ".rs", ".java",
318
+ ".json", ".yaml", ".yml", ".xml", ".html", ".css",
319
+ ".md", ".txt", ".sh", ".sql", ".csv"}:
320
+ if os.path.exists(file_path) and os.path.getsize(file_path) < 100000:
321
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
322
+ content = f.read()
323
+ if content.strip():
324
+ self._generated_files.append(FileWritten(
325
+ path=file_path,
326
+ content=content
327
+ ))
328
+ logger.debug(f"Tracked Bash-generated file: {file_path}")
329
+ except Exception as e:
330
+ logger.debug(f"Could not read Bash-generated file {file_path}: {e}")
331
+
332
+ def _augment_subagent_prompts(self, agents: dict[str, AgentDefinition] | None) -> dict[str, AgentDefinition] | None:
333
+ """Augment subagent prompts with cached code snippets."""
334
+ if not agents or not self._cache_enabled:
335
+ return agents
336
+
337
+ augmented_agents = {}
338
+ for name, agent_def in agents.items():
339
+ subagent_snippet = self._subagent_cache.get(name, "")
340
+ if subagent_snippet:
341
+ augmented_prompt = (agent_def.prompt or "") + subagent_snippet
342
+ augmented_agents[name] = AgentDefinition(
343
+ description=agent_def.description,
344
+ prompt=augmented_prompt,
345
+ tools=agent_def.tools,
346
+ model=agent_def.model,
347
+ )
348
+ else:
349
+ augmented_agents[name] = agent_def
350
+
351
+ return augmented_agents
352
+
353
+ async def _fetch_subagent_cache(self, agents: dict[str, AgentDefinition] | None) -> None:
354
+ """Pre-fetch cache snippets for all subagents."""
355
+ if not agents or not self._cache_enabled or not self._raysurfer:
356
+ return
357
+
358
+ for name, agent_def in agents.items():
359
+ try:
360
+ task = agent_def.description or name
361
+ response = await self._raysurfer.get_code_files(
362
+ task=task,
363
+ top_k=3,
364
+ min_verdict_score=0.3,
365
+ prefer_complete=True,
366
+ )
367
+ if response.files:
368
+ self._subagent_cache[name] = self._format_code_snippets(response.files)
369
+ logger.debug(f"Cached {len(response.files)} code blocks for subagent: {name}")
370
+ except Exception as e:
371
+ logger.debug(f"Failed to fetch cache for subagent {name}: {e}")
372
+
373
+ async def _augment_options_with_cache(self, task: str) -> ClaudeAgentOptions:
374
+ """Retrieve cached code, write to filesystem, and tell LLM where files are."""
375
+ if not self._cache_enabled or not self._raysurfer:
376
+ return self._options
377
+
378
+ try:
379
+ response = await self._raysurfer.get_code_files(
380
+ task=task,
381
+ top_k=5,
382
+ min_verdict_score=0.3,
383
+ prefer_complete=True,
384
+ )
385
+
386
+ if not response.files:
387
+ return self._options
388
+
389
+ self._cached_code_blocks = [
390
+ {
391
+ "code_block_id": f.code_block_id,
392
+ "filename": f.filename,
393
+ "description": f.description,
394
+ }
395
+ for f in response.files
396
+ ]
397
+
398
+ written_files = self._write_cached_files_to_disk(response.files)
399
+
400
+ if not written_files:
401
+ return self._options
402
+
403
+ cache_notice = self._format_cache_file_notice(written_files)
404
+ base_prompt = self._options.system_prompt
405
+
406
+ if isinstance(base_prompt, dict) and base_prompt.get("type") == "preset":
407
+ augmented_prompt = {
408
+ **base_prompt,
409
+ "append": base_prompt.get("append", "") + cache_notice,
410
+ }
411
+ else:
412
+ augmented_prompt = (base_prompt or "") + cache_notice
413
+
414
+ augmented_agents = self._augment_subagent_prompts(self._options.agents)
415
+
416
+ # Create new options with augmented prompt - pass through all other options
417
+ return ClaudeAgentOptions(
418
+ allowed_tools=self._options.allowed_tools,
419
+ disallowed_tools=self._options.disallowed_tools,
420
+ permission_mode=self._options.permission_mode,
421
+ system_prompt=augmented_prompt,
422
+ cwd=self._options.cwd,
423
+ add_dirs=self._options.add_dirs,
424
+ max_turns=self._options.max_turns,
425
+ model=self._options.model,
426
+ env=self._options.env,
427
+ mcp_servers=self._options.mcp_servers,
428
+ hooks=self._options.hooks,
429
+ can_use_tool=self._options.can_use_tool,
430
+ setting_sources=self._options.setting_sources,
431
+ include_partial_messages=self._options.include_partial_messages,
432
+ fork_session=self._options.fork_session,
433
+ continue_conversation=self._options.continue_conversation,
434
+ resume=self._options.resume,
435
+ agents=augmented_agents,
436
+ plugins=self._options.plugins,
437
+ enable_file_checkpointing=self._options.enable_file_checkpointing,
438
+ output_format=self._options.output_format,
439
+ sandbox=self._options.sandbox,
440
+ extra_args=self._options.extra_args,
441
+ max_buffer_size=self._options.max_buffer_size,
442
+ stderr=self._options.stderr,
443
+ user=self._options.user,
444
+ settings=self._options.settings,
445
+ permission_prompt_tool_name=self._options.permission_prompt_tool_name,
446
+ )
447
+ except Exception as e:
448
+ logger.debug(f"Failed to retrieve cached code: {e}")
449
+ return self._options
450
+
451
+ def _write_cached_files_to_disk(self, files) -> list[dict[str, str]]:
452
+ """Write cached code files to the working directory."""
453
+ written_files = []
454
+ cwd = self._options.cwd or os.getcwd()
455
+
456
+ cache_dir = os.path.join(cwd, "raysurfer_relevant_workflow_cache")
457
+
458
+ # Clear existing cache to avoid stale files from previous runs
459
+ if os.path.exists(cache_dir):
460
+ shutil.rmtree(cache_dir)
461
+ os.makedirs(cache_dir)
462
+
463
+ for f in files:
464
+ try:
465
+ file_path = os.path.join(cache_dir, f.filename)
466
+ os.makedirs(os.path.dirname(file_path), exist_ok=True) if os.path.dirname(file_path) != cache_dir else None
467
+
468
+ with open(file_path, 'w', encoding='utf-8') as out_file:
469
+ out_file.write(f.source)
470
+
471
+ written_files.append({
472
+ "path": file_path,
473
+ "filename": f.filename,
474
+ "description": f.description,
475
+ "entrypoint": f.entrypoint,
476
+ "language": f.language,
477
+ "confidence": f.verdict_score,
478
+ })
479
+ logger.debug(f"Wrote cached file: {file_path}")
480
+ except Exception as e:
481
+ logger.debug(f"Failed to write cached file {f.filename}: {e}")
482
+
483
+ return written_files
484
+
485
+ def _format_cache_file_notice(self, written_files: list[dict[str, str]]) -> str:
486
+ """Format a minimal notice telling the LLM where cached files are."""
487
+ notice = "\n\n## IMPORTANT: Pre-validated Workflow Code Available - USE THESE FILES\n\n"
488
+ notice += "**DO NOT REWRITE CODE.** The following validated workflow code has been retrieved from the cloud cache "
489
+ notice += "and saved to `raysurfer_relevant_workflow_cache/`. Use these files directly:\n\n"
490
+
491
+ for f in written_files:
492
+ notice += f"### `{f['filename']}` → `{f['path']}`\n"
493
+ notice += f"- **What it does**: {f['description']}\n"
494
+ notice += f"- **Run with**: `python {f['path']}`\n"
495
+ notice += f"- **Copy to destination**: `cp {f['path']} ./`\n\n"
496
+
497
+ notice += "**Instructions**:\n"
498
+ notice += "1. If the task asks to create a file that matches one above, just COPY it: `cp <cached_path> <destination>`\n"
499
+ notice += "2. If you need to execute, run the cached file directly: `python <cached_path>`\n"
500
+ notice += "3. Only Read and modify if the task requires changes not covered by the cached file\n"
501
+ notice += "4. **DO NOT regenerate code that already exists** - this wastes time\n"
502
+
503
+ return notice
504
+
505
+ def _format_code_snippets(self, files) -> str:
506
+ """Format cached code files as markdown for system prompt."""
507
+ snippets = "\n\n## Cached Code (from Raysurfer)\n\n"
508
+ snippets += "The following pre-validated code is available for this task. "
509
+ snippets += "Use it directly or adapt it as needed.\n\n"
510
+
511
+ for f in files:
512
+ snippets += f"### {f.filename}\n"
513
+ snippets += f"**Description**: {f.description}\n"
514
+ snippets += f"**Entrypoint**: `{f.entrypoint}`\n"
515
+ snippets += f"**Confidence**: {f.verdict_score:.0%}\n\n"
516
+ snippets += f"```{f.language}\n{f.source}\n```\n\n"
517
+
518
+ return snippets
519
+
520
+ async def _upload_to_cache(self) -> None:
521
+ """Upload generated code to the Raysurfer cache."""
522
+ if not self._raysurfer or not self._current_query:
523
+ return
524
+
525
+ try:
526
+ result = await self._raysurfer.submit_execution_result(
527
+ task=self._current_query,
528
+ files_written=self._generated_files,
529
+ succeeded=self._task_succeeded,
530
+ )
531
+ if result.code_blocks_stored > 0:
532
+ logger.info(f"Cached {result.code_blocks_stored} code blocks")
533
+ except Exception as e:
534
+ logger.debug(f"Failed to upload to cache: {e}")
535
+
536
+ async def _submit_votes(self) -> None:
537
+ """Submit thumbs up votes for cached code blocks that helped complete the task."""
538
+ if not self._raysurfer or not self._current_query:
539
+ return
540
+
541
+ for block in self._cached_code_blocks:
542
+ try:
543
+ await self._raysurfer.record_cache_usage(
544
+ task=self._current_query,
545
+ code_block_id=block["code_block_id"],
546
+ code_block_name=block["filename"],
547
+ code_block_description=block["description"],
548
+ succeeded=self._task_succeeded,
549
+ )
550
+ logger.debug(f"Submitted vote for {block['filename']}")
551
+ except Exception as e:
552
+ logger.debug(f"Failed to submit vote for {block['filename']}: {e}")
raysurfer/sdk_types.py ADDED
@@ -0,0 +1,30 @@
1
+ """Types for Claude Agent SDK integration"""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class CodeFile(BaseModel):
9
+ """A code file ready to be written to sandbox"""
10
+
11
+ code_block_id: str
12
+ filename: str # e.g., "github_fetcher.py"
13
+ source: str # Full source code
14
+ entrypoint: str # Function to call
15
+ description: str
16
+ input_schema: dict[str, Any] = Field(default_factory=dict)
17
+ output_schema: dict[str, Any] = Field(default_factory=dict)
18
+ language: str
19
+ dependencies: list[str] = Field(default_factory=list)
20
+ verdict_score: float
21
+ thumbs_up: int
22
+ thumbs_down: int
23
+
24
+
25
+ class GetCodeFilesResponse(BaseModel):
26
+ """Response with code files for a task"""
27
+
28
+ files: list[CodeFile]
29
+ task: str
30
+ total_found: int