scry-run 0.1.0__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.
scry_run/generator.py ADDED
@@ -0,0 +1,698 @@
1
+ """Code generator using pluggable LLM backends with structured JSON output."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ import os
7
+ import sys
8
+ import time
9
+ import random
10
+ import json
11
+ from dataclasses import dataclass
12
+ from typing import Optional
13
+
14
+ from scry_run.console import warning
15
+
16
+
17
+ @dataclass
18
+ class GeneratedCode:
19
+ """Result of code generation."""
20
+
21
+ code: str
22
+ code_type: str # method, property, classmethod, staticmethod
23
+ docstring: str
24
+ dependencies: list[str]
25
+ packages: list[str] = None # PyPI packages to install
26
+
27
+ def __post_init__(self):
28
+ if self.packages is None:
29
+ self.packages = []
30
+
31
+
32
+ # JSON schema for structured output
33
+ CODE_GENERATION_SCHEMA = {
34
+ "type": "object",
35
+ "properties": {
36
+ "code": {
37
+ "type": "string",
38
+ "description": "The Python code for the requested method/property. Must be valid Python that can be exec'd."
39
+ },
40
+ "code_type": {
41
+ "type": "string",
42
+ "enum": ["method", "property", "classmethod", "staticmethod"],
43
+ "description": "The type of code being generated"
44
+ },
45
+ "docstring": {
46
+ "type": "string",
47
+ "description": "A brief description of what this code does"
48
+ },
49
+ "dependencies": {
50
+ "type": "array",
51
+ "items": {"type": "string"},
52
+ "description": "List of import statements needed (e.g., 'import json', 'from pathlib import Path')"
53
+ },
54
+ "packages": {
55
+ "type": "array",
56
+ "items": {"type": "string"},
57
+ "description": "List of PyPI packages to install (e.g., 'pygame', 'requests>=2.0'). Only include packages not in the standard library."
58
+ }
59
+ },
60
+ "required": ["code", "code_type", "docstring", "dependencies", "packages"]
61
+ }
62
+
63
+
64
+ # =============================================================================
65
+ # User-Friendly Exception Classes
66
+ # =============================================================================
67
+
68
+ class ScryRunError(Exception):
69
+ """Base exception for scry-run errors with user-friendly messages."""
70
+
71
+ def __init__(self, message: str, hint: Optional[str] = None):
72
+ self.message = message
73
+ self.hint = hint
74
+ super().__init__(self._format_message())
75
+
76
+ def _format_message(self) -> str:
77
+ msg = f"[scry-run] {self.message}"
78
+ if self.hint:
79
+ msg += f"\n 💡 {self.hint}"
80
+ return msg
81
+
82
+
83
+ class APIKeyError(ScryRunError):
84
+ """API key is missing or invalid."""
85
+ pass
86
+
87
+
88
+ class RateLimitError(ScryRunError):
89
+ """Rate limit exceeded."""
90
+ pass
91
+
92
+
93
+ class QuotaExceededError(ScryRunError):
94
+ """API quota exhausted."""
95
+ pass
96
+
97
+
98
+ class ModelNotFoundError(ScryRunError):
99
+ """Requested model doesn't exist or isn't accessible."""
100
+ pass
101
+
102
+
103
+ class ContentBlockedError(ScryRunError):
104
+ """Content was blocked by safety filters."""
105
+ pass
106
+
107
+
108
+ class NetworkError(ScryRunError):
109
+ """Network connectivity issue."""
110
+ pass
111
+
112
+
113
+ class CodeGenerationError(ScryRunError):
114
+ """Code generation failed."""
115
+ pass
116
+
117
+
118
+ class CodeValidationError(ScryRunError):
119
+ """Generated code failed to parse."""
120
+ pass
121
+
122
+
123
+ def _handle_api_error(error: Exception, model: str) -> ScryRunError:
124
+ """Convert API exceptions to user-friendly errors.
125
+
126
+ Args:
127
+ error: The original exception from the backend
128
+ model: The model name being used (for error messages)
129
+
130
+ Returns:
131
+ A user-friendly ScryRunError subclass
132
+ """
133
+ error_str = str(error).lower()
134
+ error_type = type(error).__name__
135
+
136
+ # Authentication errors
137
+ if "api_key" in error_str or "authentication" in error_str or "401" in error_str:
138
+ return APIKeyError(
139
+ "Invalid API key",
140
+ "Check your API key configuration"
141
+ )
142
+
143
+ # Rate limiting
144
+ if "rate" in error_str and "limit" in error_str or "429" in error_str or "resource_exhausted" in error_str:
145
+ return RateLimitError(
146
+ "Rate limit exceeded",
147
+ "Wait a moment and try again. Consider using a slower request rate."
148
+ )
149
+
150
+ # Quota exceeded
151
+ if "quota" in error_str or "billing" in error_str:
152
+ return QuotaExceededError(
153
+ "API quota exhausted",
154
+ "Check your API quota and billing settings"
155
+ )
156
+
157
+ # Model not found
158
+ if "model" in error_str and ("not found" in error_str or "not exist" in error_str or "404" in error_str):
159
+ return ModelNotFoundError(
160
+ f"Model '{model}' not found",
161
+ f"Try a different model. Set SCRY_MODEL or backend-specific env var."
162
+ )
163
+
164
+ # Content blocked by safety filters
165
+ if "safety" in error_str or "blocked" in error_str or "harm" in error_str:
166
+ return ContentBlockedError(
167
+ "Content blocked by safety filters",
168
+ "Try rephrasing your class/method names or docstrings"
169
+ )
170
+
171
+ # Network errors
172
+ if any(x in error_str for x in ["connection", "timeout", "network", "dns", "ssl"]):
173
+ return NetworkError(
174
+ "Network error connecting to backend",
175
+ "Check your internet connection and firewall settings"
176
+ )
177
+
178
+ # Permission/access errors
179
+ if "permission" in error_str or "403" in error_str or "access" in error_str:
180
+ return APIKeyError(
181
+ "API access denied",
182
+ "Check your API key permissions"
183
+ )
184
+
185
+ # Server errors
186
+ if "500" in error_str or "503" in error_str or "server" in error_str:
187
+ return NetworkError(
188
+ "Backend server error",
189
+ "The API is temporarily unavailable. Try again in a few seconds."
190
+ )
191
+
192
+ # Fallback: wrap unknown errors
193
+ return CodeGenerationError(
194
+ f"API error: {error_type}: {error}",
195
+ "Check the error message above. If persistent, file an issue."
196
+ )
197
+
198
+
199
+ class GenerationLimitError(ScryRunError):
200
+ """Global generation limit exceeded."""
201
+ pass
202
+
203
+
204
+ class CodeGenerator:
205
+ """Generates code using pluggable backends.
206
+
207
+ The generator maintains a persistent session with the backend.
208
+ Full codebase context is sent once at startup, then each generation
209
+ request sends only the minimal "frame" (class/method info).
210
+
211
+ Configure via SCRY_BACKEND env var or backend parameter.
212
+ """
213
+
214
+ MAX_RETRIES = 5
215
+
216
+ # Global counter for all generations in this process
217
+ _total_generations_count = 0
218
+
219
+ # Track context per-generator instance
220
+ _context_hash: Optional[str] = None
221
+
222
+ def __init__(
223
+ self,
224
+ api_key: Optional[str] = None,
225
+ model: Optional[str] = None,
226
+ backend: Optional[str] = None,
227
+ ):
228
+ """Initialize the code generator.
229
+
230
+ Args:
231
+ api_key: Deprecated, not used.
232
+ model: Model to use. Defaults to backend-specific default.
233
+ backend: Backend to use: "claude", "frozen", or "auto" (default).
234
+ Can also be set via SCRY_BACKEND env var.
235
+ """
236
+ from scry_run.backends import get_backend
237
+
238
+ # Pass model through - backends handle their own defaults
239
+ self.model = model or os.environ.get("SCRY_MODEL")
240
+ self._backend = get_backend(name=backend, model=self.model)
241
+ self._context_provided = False
242
+
243
+
244
+ def _format_installed_packages(self, packages: list[str] | None) -> str:
245
+ """Format installed packages for the prompt."""
246
+ if not packages:
247
+ return "No packages are currently installed in this app's environment."
248
+
249
+ pkg_list = "\n".join(f"- {pkg}" for pkg in packages)
250
+ return f"""The following packages are already installed:
251
+ {pkg_list}
252
+
253
+ Prefer using these before requesting new packages. Only add to "packages" if you need something not listed above."""
254
+
255
+ def _build_context_prompt(
256
+ self,
257
+ context: str,
258
+ installed_packages: list[str] | None = None,
259
+ ) -> str:
260
+ """Build the full context prompt (sent once at session start).
261
+
262
+ Args:
263
+ context: Full codebase context with hole marker
264
+ installed_packages: Packages already installed
265
+
266
+ Returns:
267
+ The formatted context string for the system prompt
268
+ """
269
+ return f"""## Codebase Context
270
+
271
+ The following is the full codebase you are working with:
272
+
273
+ ```python
274
+ {context}
275
+ ```
276
+
277
+ ## Installed Packages
278
+
279
+ {self._format_installed_packages(installed_packages)}
280
+
281
+ ## Critical Requirements for Code Generation
282
+
283
+ ### Code Quality
284
+ 1. The code MUST be valid Python that can be executed with `exec()`
285
+ 2. Complete method/property definition starting with `def` or `@property`
286
+ 3. Use appropriate decorators (@classmethod, @staticmethod, @property) if needed
287
+ 4. STRICTLY USE TYPE HINTS for all parameters and return values
288
+
289
+ ### Application-Level Thinking
290
+ 5. READ THE CLASS DOCSTRING - it describes what the application does
291
+ 6. EXTRAPOLATE sensibly based on the application's purpose
292
+ 7. Generate code that a HUMAN would write for this application
293
+
294
+ ### Dependencies
295
+ 8. List ALL imports needed in the dependencies field
296
+ 9. Prefer standard library when possible
297
+ """
298
+
299
+ def _build_frame_prompt(
300
+ self,
301
+ class_name: str,
302
+ attr_name: str,
303
+ is_classmethod: bool = False,
304
+ runtime_context: str = "",
305
+ ) -> str:
306
+ """Build the minimal frame prompt (sent for each generation).
307
+
308
+ This assumes the full codebase has already been provided.
309
+ Runtime context (call stack, variables) is included since it's dynamic.
310
+
311
+ Args:
312
+ class_name: Name of the class needing the attribute
313
+ attr_name: Name of the attribute to generate
314
+ is_classmethod: Whether this is a class-level attribute
315
+ runtime_context: Dynamic context (call stack, local variables)
316
+
317
+ Returns:
318
+ The minimal prompt for this specific generation
319
+ """
320
+ attr_type = "class method/property" if is_classmethod else "instance method/property"
321
+
322
+ # Include runtime context if provided
323
+ context_section = ""
324
+ if runtime_context:
325
+ context_section = f"\n## Runtime Context\n\n{runtime_context}\n"
326
+
327
+ return f"""Generate the `{attr_name}` {attr_type} for the `{class_name}` class.
328
+ {context_section}
329
+ Respond with a JSON object containing:
330
+ - code: The Python code (complete method/property definition with type hints)
331
+ - code_type: One of "method", "property", "classmethod", "staticmethod"
332
+ - docstring: Brief description of what this code does
333
+ - dependencies: List of import statements needed
334
+ - packages: List of PyPI packages to install (if any external packages needed)
335
+
336
+ IMPORTANT: Output ONLY the JSON object, no markdown, no explanation, no code fences."""
337
+
338
+ def _build_prompt(
339
+ self,
340
+ context: str,
341
+ class_name: str,
342
+ attr_name: str,
343
+ is_classmethod: bool = False,
344
+ installed_packages: list[str] | None = None,
345
+ ) -> str:
346
+ """Build the full prompt for code generation (legacy/fallback).
347
+
348
+ This is used when the backend doesn't support set_context().
349
+
350
+ Args:
351
+ context: Full codebase context with hole marker
352
+ class_name: Name of the class needing the attribute
353
+ attr_name: Name of the attribute to generate
354
+ is_classmethod: Whether this is a class-level attribute
355
+
356
+ Returns:
357
+ The formatted prompt string
358
+ """
359
+ attr_type = "class method/property" if is_classmethod else "instance method/property"
360
+
361
+ return f"""You are an expert Python code generator for the scry-run dynamic code generation system.
362
+
363
+ ## Your Role
364
+
365
+ You are generating code for an APPLICATION, not just isolated functions. Read the class docstring carefully - it describes the PURPOSE of the entire application. Use this to EXTRAPOLATE what this method should do.
366
+
367
+ ## Codebase Context
368
+
369
+ The following is the full codebase. There is a marker showing where code is missing:
370
+
371
+ ```python
372
+ {context}
373
+ ```
374
+
375
+ ## Task
376
+
377
+ Generate the `{attr_name}` {attr_type} for the `{class_name}` class.
378
+
379
+ ## Critical Requirements
380
+
381
+ ### Code Quality
382
+ 1. The code MUST be valid Python that can be executed with `exec()`
383
+ 2. Complete method/property definition starting with `def` or `@property`
384
+ 3. Use appropriate decorators (@classmethod, @staticmethod, @property) if needed
385
+ 4. STRICTLY USE TYPE HINTS for all parameters and return values (e.g. `def foo(x: int) -> str:`)
386
+ - Import `typing` types (List, Dict, Optional, Any) as needed and add to dependencies
387
+
388
+ ### Rich Contextual Comments
389
+ 4. Include a DETAILED docstring explaining:
390
+ - What this method does
391
+ - How it fits into the overall application described in the class docstring
392
+ - Any assumptions made about the application's behavior
393
+ 5. Add inline comments for non-obvious logic
394
+ 6. Document parameter types and return types
395
+
396
+ ### Application-Level Thinking
397
+ 7. READ THE CLASS DOCSTRING - it describes what the application does
398
+ 8. EXTRAPOLATE sensibly: if the app is a "todo list", a `save()` method should persist todos
399
+ 9. Infer reasonable behavior from:
400
+ - The application description (class docstring)
401
+ - The method/property name
402
+ - Other existing methods
403
+ - How similar applications typically work
404
+ 10. Generate code that a HUMAN would write for this application, not minimal stubs
405
+ 11. Think about what OTHER PARAMETERS this method might need beyond the obvious ones:
406
+ - Configuration options (e.g., format, encoding, verbosity)
407
+ - Optional filters or modifiers
408
+ - Callbacks or hooks for extensibility
409
+ - Make these optional with sensible defaults
410
+
411
+ ### Dependencies
412
+ 11. List ALL imports needed in the dependencies field
413
+ 12. Prefer standard library when possible
414
+ 13. The code should work standalone once dependencies are imported
415
+ 14. Prefer standard library when possible
416
+ 15. The code should work standalone once dependencies are imported
417
+
418
+ ### Packages
419
+
420
+ The app has its own virtual environment. If you need external packages:
421
+ 1. Add import statements to "dependencies" (e.g., "import pygame")
422
+ 2. Add PyPI package names to "packages" (e.g., "pygame")
423
+
424
+ {self._format_installed_packages(installed_packages)}
425
+
426
+ ## Output Format
427
+
428
+ Respond with a JSON object containing:
429
+ - code: The Python code (complete method/property definition with rich comments)
430
+ - code_type: One of "method", "property", "classmethod", "staticmethod"
431
+ - docstring: Brief description of what this code does
432
+ - dependencies: List of import statements needed
433
+ - packages: List of PyPI packages to install (if any external packages needed)
434
+
435
+ IMPORTANT: Output ONLY the JSON object, no markdown, no explanation, no code fences."""
436
+
437
+ def _fix_indentation(self, code: str) -> str:
438
+ """Fix common indentation issues in generated code.
439
+
440
+ The LLM sometimes returns code with leading indentation or
441
+ inconsistent whitespace. This method normalizes it.
442
+
443
+ Args:
444
+ code: Raw generated code
445
+
446
+ Returns:
447
+ Code with fixed indentation
448
+ """
449
+ import textwrap
450
+ from scry_run.logging import get_logger
451
+ logger = get_logger()
452
+
453
+ original_code = code
454
+
455
+ # Strip leading/trailing empty lines
456
+ lines = code.split('\n')
457
+ while lines and not lines[0].strip():
458
+ lines.pop(0)
459
+ while lines and not lines[-1].strip():
460
+ lines.pop()
461
+
462
+ if not lines:
463
+ return code
464
+
465
+ # Check if first non-empty line is indented
466
+ first_line = lines[0]
467
+ if first_line and first_line[0] in ' \t':
468
+ # Use textwrap.dedent to remove common leading whitespace
469
+ code = textwrap.dedent('\n'.join(lines))
470
+ logger.debug("Fixed indentation in generated code", f"ORIGINAL:\n{original_code}\n\nFIXED:\n{code}")
471
+ else:
472
+ code = '\n'.join(lines)
473
+
474
+ return code
475
+
476
+ def _validate_code(self, code: str) -> None:
477
+ """Validate that the code parses correctly.
478
+
479
+ Args:
480
+ code: Python code to validate
481
+
482
+ Raises:
483
+ CodeValidationError: If code fails to parse
484
+ """
485
+ try:
486
+ ast.parse(code)
487
+ except SyntaxError as e:
488
+ raise CodeValidationError(f"Generated code has syntax error: {e}")
489
+
490
+
491
+ def _parse_response(self, response_text: str) -> GeneratedCode:
492
+ """Parse the JSON response from the LLM.
493
+
494
+ Args:
495
+ response_text: Raw response text from LLM
496
+
497
+ Returns:
498
+ GeneratedCode object
499
+
500
+ Raises:
501
+ CodeGenerationError: If response cannot be parsed
502
+ """
503
+ try:
504
+ data = json.loads(response_text)
505
+ except json.JSONDecodeError as e:
506
+ raise CodeGenerationError(f"Failed to parse JSON response: {e}")
507
+
508
+ required_fields = ["code", "code_type", "docstring", "dependencies", "packages"]
509
+ for field in required_fields:
510
+ if field not in data:
511
+ raise CodeGenerationError(f"Missing required field: {field}")
512
+
513
+ return GeneratedCode(
514
+ code=data["code"],
515
+ code_type=data["code_type"],
516
+ docstring=data["docstring"],
517
+ dependencies=data["dependencies"],
518
+ packages=data["packages"],
519
+ )
520
+ def generate(
521
+ self,
522
+ context: str,
523
+ class_name: str,
524
+ attr_name: str,
525
+ is_classmethod: bool = False,
526
+ installed_packages: list[str] | None = None,
527
+ ) -> GeneratedCode:
528
+ """Generate code for a missing attribute.
529
+
530
+ Args:
531
+ context: Full codebase context with hole marker
532
+ class_name: Name of the class needing the attribute
533
+ attr_name: Name of the attribute to generate
534
+ is_classmethod: Whether this is a class-level attribute
535
+ installed_packages: List of packages already installed in the app's venv
536
+
537
+ Returns:
538
+ GeneratedCode with the generated code and metadata
539
+
540
+ Raises:
541
+ CodeGenerationError: If generation fails after retries
542
+ CodeValidationError: If generated code doesn't parse
543
+ APIKeyError: If API key is invalid
544
+ RateLimitError: If rate limit is exceeded
545
+ QuotaExceededError: If quota is exhausted
546
+ NetworkError: If network issues occur
547
+ GenerationLimitError: If global generation limit is exceeded
548
+ """
549
+ # Check global generation limit
550
+ max_limit_str = os.environ.get("SCRY_MAX_GENERATIONS", "100")
551
+ if max_limit_str:
552
+ try:
553
+ limit = int(max_limit_str)
554
+ if CodeGenerator._total_generations_count >= limit:
555
+ raise GenerationLimitError(
556
+ f"Global generation limit of {limit} reached.",
557
+ f"Increase SCRY_MAX_GENERATIONS env var if this is intended."
558
+ )
559
+ except ValueError:
560
+ # Ignore invalid integer values in env var
561
+ pass
562
+
563
+ # Increment counter
564
+ CodeGenerator._total_generations_count += 1
565
+
566
+ # Log generation start
567
+ from scry_run.logging import get_logger
568
+ logger = get_logger()
569
+
570
+ # Set generation context for frozen backend error messages
571
+ if hasattr(self._backend, 'set_generation_context'):
572
+ self._backend.set_generation_context(class_name, attr_name)
573
+
574
+ # Check if backend supports persistent context
575
+ supports_context = hasattr(self._backend, 'set_context')
576
+
577
+ # Extract runtime context from the full context (it's appended at the end)
578
+ # The runtime context is dynamic and must be included in every request
579
+ runtime_context = ""
580
+ if "# === RUNTIME CONTEXT ===" in context:
581
+ idx = context.find("# === RUNTIME CONTEXT ===")
582
+ runtime_context = context[idx:]
583
+
584
+ if supports_context and not self._context_provided:
585
+ # First generation: send full context to backend (without runtime context)
586
+ # Runtime context is dynamic and will be sent with each frame
587
+ static_context = context[:context.find("# === RUNTIME CONTEXT ===")] if runtime_context else context
588
+ context_prompt = self._build_context_prompt(static_context, installed_packages)
589
+ self._backend.set_context(context_prompt)
590
+ self._context_provided = True
591
+ logger.info("Generator: Provided full context to backend session")
592
+
593
+ # For this generation, send frame with runtime context
594
+ prompt = self._build_frame_prompt(class_name, attr_name, is_classmethod, runtime_context)
595
+ elif supports_context:
596
+ # Subsequent generations: send frame with runtime context
597
+ prompt = self._build_frame_prompt(class_name, attr_name, is_classmethod, runtime_context)
598
+ else:
599
+ # Backend doesn't support persistent context, use full prompt
600
+ prompt = self._build_prompt(context, class_name, attr_name, is_classmethod, installed_packages)
601
+
602
+ logger.log_generation_start(class_name, attr_name, prompt)
603
+
604
+ # Delegate to backend with retries
605
+ last_error: Optional[Exception] = None
606
+
607
+ for attempt in range(self.MAX_RETRIES):
608
+ try:
609
+ result = self._backend.generate_code(prompt)
610
+
611
+ # Log raw response
612
+ logger.log_generation_response(result.code if hasattr(result, 'code') else str(result))
613
+
614
+ # Convert backend result to our GeneratedCode type
615
+ from scry_run.backends.base import GeneratedCode as BackendResult
616
+ if isinstance(result, BackendResult):
617
+ result = GeneratedCode(
618
+ code=result.code,
619
+ code_type=result.code_type,
620
+ docstring=result.docstring,
621
+ dependencies=result.dependencies,
622
+ packages=result.packages,
623
+ )
624
+
625
+ # Fix indentation issues before validation
626
+ result = GeneratedCode(
627
+ code=self._fix_indentation(result.code),
628
+ code_type=result.code_type,
629
+ docstring=result.docstring,
630
+ dependencies=result.dependencies,
631
+ packages=result.packages,
632
+ )
633
+
634
+ # Validate the generated code
635
+ self._validate_code(result.code)
636
+
637
+
638
+ # Log success
639
+ logger.log_generation_success(class_name, attr_name, result.code)
640
+
641
+ return result
642
+
643
+ except (CodeValidationError, CodeGenerationError) as e:
644
+ last_error = e
645
+ # Log the validation error with full details
646
+ code_str = result.code if 'result' in dir() and hasattr(result, 'code') else None
647
+ logger.log_validation_error(e, code_str)
648
+
649
+ if attempt >= self.MAX_RETRIES - 1:
650
+ raise
651
+ warning(f"Validation error, retrying ({attempt + 1}/{self.MAX_RETRIES})...")
652
+ except Exception as e:
653
+ # Log the error
654
+ logger.error(f"Backend error: {e}", str(e))
655
+
656
+ # Convert to friendly error
657
+ friendly_error = _handle_api_error(e, self.model)
658
+ last_error = friendly_error
659
+
660
+ if isinstance(friendly_error, (RateLimitError, NetworkError)):
661
+ wait_time = 2 ** attempt
662
+ warning(f"{friendly_error.message}. Retrying in {wait_time:.1f}s...")
663
+ time.sleep(wait_time)
664
+ else:
665
+ raise friendly_error
666
+
667
+ raise CodeGenerationError(
668
+ f"Failed to generate valid code after {self.MAX_RETRIES} attempts",
669
+ f"Last error: {last_error}"
670
+ )
671
+
672
+
673
+ def generate_freeform(
674
+ self,
675
+ prompt: str,
676
+ system_instruction: Optional[str] = "You are a helpful coding assistant.",
677
+ ) -> str:
678
+ """Generate freeform text from a prompt.
679
+
680
+ Args:
681
+ prompt: User prompt
682
+ system_instruction: System instruction to guide the model behavior
683
+
684
+ Returns:
685
+ Generated text content
686
+
687
+ Raises:
688
+ ScryRunError: On API errors
689
+ """
690
+ # Prepend system instruction if provided
691
+ full_prompt = prompt
692
+ if system_instruction:
693
+ full_prompt = f"{system_instruction}\n\n{prompt}"
694
+
695
+ try:
696
+ return self._backend.generate_text(full_prompt)
697
+ except Exception as e:
698
+ raise _handle_api_error(e, self.model)