diff --git a/.gitignore b/.gitignore index 2e9b9237..44dc1360 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,7 @@ cython_debug/ # PyPI configuration file .pypirc .aider* + +# AI Assistant Ignores +CLAUDE.md +.codex/ \ No newline at end of file diff --git a/examples/basic/file_instructions_example.py b/examples/basic/file_instructions_example.py new file mode 100644 index 00000000..60427119 --- /dev/null +++ b/examples/basic/file_instructions_example.py @@ -0,0 +1,41 @@ +""" +Example: Loading agent instructions from a text file using load_instructions_from_file. +""" +import asyncio +from pathlib import Path + +from pydantic import BaseModel + +from agents import Agent, Runner +from agents.extensions.file_utils import load_instructions_from_file + + +# Define expected output schema +class Greeting(BaseModel): + greeting: str + greeting_spanish: str + + +async def main(): + # Locate and load instructions from file + inst_path = Path(__file__).parent / "greet_instructions.txt" + instructions = load_instructions_from_file(str(inst_path)) + + # Create agent with file-based instructions + greeter = Agent( + name="Greeting Agent", + instructions=instructions, + output_type=Greeting, + ) + + # Prompt user for name and run + name = input("Enter your name: ") + result = await Runner.run(greeter, name) + + # result.final_output is parsed into Greeting model + print("JSON output:", result.final_output.model_dump_json()) + print("Greeting message:", result.final_output) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/basic/greet_instructions.txt b/examples/basic/greet_instructions.txt new file mode 100644 index 00000000..e86caee9 --- /dev/null +++ b/examples/basic/greet_instructions.txt @@ -0,0 +1,8 @@ +# Instructions for GreetingAgent + +Given an input string that represents a person's name, generate a JSON object with keys "greeting" and "greeting_spanish" whose value is a polite greeting message, formatted as: + +{ + "greeting": "Hello, !", + "greeting_spanish": "Hola, !" +} diff --git a/src/agents/extensions/file_utils.py b/src/agents/extensions/file_utils.py new file mode 100644 index 00000000..52512641 --- /dev/null +++ b/src/agents/extensions/file_utils.py @@ -0,0 +1,61 @@ +""" +Helper utilities for file-based operations, e.g. loading instruction text files. +""" + +from collections.abc import Sequence +from pathlib import Path + + +class InstructionFileError(Exception): + """Base exception for load_instructions_from_file errors.""" + +class InstructionFileNotFoundError(InstructionFileError): + """Raised when the file does not exist or is not a file.""" + +class InvalidFileTypeError(InstructionFileError): + """Raised when the file extension is not allowed.""" + +class FileTooLargeError(InstructionFileError): + """Raised when the file size exceeds the maximum allowed.""" + +def load_instructions_from_file( + path: str, + encoding: str = "utf-8", + allowed_extensions: Sequence[str] = (".txt", ".md"), + max_size_bytes: int = 1 * 1024 * 1024, +) -> str: + """Load a text file with strict validations and return its contents. + + Args: + path: Path to the instruction file. + encoding: File encoding (defaults to 'utf-8'). + allowed_extensions: Tuple of allowed file extensions. + max_size_bytes: Maximum allowed file size in bytes. + + Returns: + The file contents as a string. + + Raises: + InstructionFileNotFoundError: if the file does not exist or is not a file. + InvalidFileTypeError: if the file extension is not in allowed_extensions. + FileTooLargeError: if the file size exceeds max_size_bytes. + InstructionFileError: for IO or decoding errors. + """ + file_path = Path(path) + if not file_path.is_file(): + raise InstructionFileNotFoundError(f"File not found or is not a file: {file_path}") + if file_path.suffix.lower() not in allowed_extensions: + raise InvalidFileTypeError( + f"Invalid file extension {file_path.suffix!r}, allowed: {allowed_extensions}" + ) + size = file_path.stat().st_size + if size > max_size_bytes: + raise FileTooLargeError( + f"File size {size} exceeds maximum {max_size_bytes} bytes" + ) + try: + return file_path.read_text(encoding=encoding) + except UnicodeDecodeError as e: + raise InstructionFileError(f"Could not decode file {file_path}: {e}") from e + except OSError as e: + raise InstructionFileError(f"Error reading file {file_path}: {e}") from e diff --git a/tests/test_file_utils.py b/tests/test_file_utils.py new file mode 100644 index 00000000..49c11aa5 --- /dev/null +++ b/tests/test_file_utils.py @@ -0,0 +1,49 @@ + +import pytest + +from agents.extensions.file_utils import ( + FileTooLargeError, + InstructionFileError, + InstructionFileNotFoundError, + InvalidFileTypeError, + load_instructions_from_file, +) + + +def test_successful_read(tmp_path): + file = tmp_path / "example.txt" + content = "This is a test." + file.write_text(content, encoding="utf-8") + result = load_instructions_from_file(str(file)) + assert result == content + + +def test_file_not_found(tmp_path): + file = tmp_path / "nonexistent.txt" + with pytest.raises(InstructionFileNotFoundError): + load_instructions_from_file(str(file)) + + +def test_invalid_extension(tmp_path): + file = tmp_path / "example.bin" + file.write_text("data", encoding="utf-8") + with pytest.raises(InvalidFileTypeError) as exc: + load_instructions_from_file(str(file)) + assert ".bin" in str(exc.value) + + +def test_file_too_large(tmp_path): + file = tmp_path / "example.txt" + content = "a" * 20 + file.write_text(content, encoding="utf-8") + with pytest.raises(FileTooLargeError) as exc: + load_instructions_from_file(str(file), max_size_bytes=10) + assert "exceeds maximum" in str(exc.value) + + +def test_decode_error(tmp_path): + file = tmp_path / "example.txt" + file.write_bytes(b'\xff\xfe\xfd') + with pytest.raises(InstructionFileError) as exc: + load_instructions_from_file(str(file), encoding="utf-8") + assert "Could not decode file" in str(exc.value)