diff --git a/py/packages/genkit/src/genkit/ai/_registry.py b/py/packages/genkit/src/genkit/ai/_registry.py index bc98b0ab09..e186bdaf82 100644 --- a/py/packages/genkit/src/genkit/ai/_registry.py +++ b/py/packages/genkit/src/genkit/ai/_registry.py @@ -296,6 +296,7 @@ def define_indexer( description: Optional description for the indexer. """ indexer_meta = metadata if metadata else {} + if 'indexer' not in indexer_meta: indexer_meta['indexer'] = {} if 'label' not in indexer_meta['indexer'] or not indexer_meta['indexer']['label']: @@ -323,7 +324,7 @@ def define_evaluator( metadata: dict[str, Any] | None = None, description: str | None = None, ) -> Action: - """Define a evaluator action. + """Define an evaluator action. This action runs the callback function on the every sample of the input dataset. @@ -607,6 +608,38 @@ def define_prompt( use=use, ) + async def prompt( + self, + name: str, + variant: str | None = None, + ): + """Look up a prompt by name and optional variant. + + This matches the JavaScript prompt() function behavior. + + Can look up prompts that were: + 1. Defined programmatically using define_prompt() + 2. Loaded from .prompt files using load_prompt_folder() + + Args: + registry: The registry to look up the prompt from. + name: The name of the prompt. + variant: Optional variant name. + dir: Optional directory parameter (accepted for compatibility but not used). + + Returns: + An ExecutablePrompt instance. + + Raises: + GenkitError: If the prompt is not found. + """ + + return await lookup_prompt( + registry=self.registry, + name=name, + variant=variant, + ) + class FlowWrapper: """A wapper for flow functions to add `stream` method.""" diff --git a/py/packages/genkit/src/genkit/blocks/prompt.py b/py/packages/genkit/src/genkit/blocks/prompt.py index 4589de0d1c..6bd8dfe242 100644 --- a/py/packages/genkit/src/genkit/blocks/prompt.py +++ b/py/packages/genkit/src/genkit/blocks/prompt.py @@ -22,10 +22,14 @@ generation and management across different parts of the application. """ +import os +import weakref from asyncio import Future -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Callable +from pathlib import Path from typing import Any +import structlog from dotpromptz.typing import ( DataArgument, PromptFunction, @@ -45,15 +49,19 @@ GenerateResponseWrapper, ModelMiddleware, ) -from genkit.core.action import ActionRunContext +from genkit.core.action import Action, ActionRunContext, create_action_key +from genkit.core.action.types import ActionKind +from genkit.core.error import GenkitError from genkit.core.registry import Registry from genkit.core.schema import to_json_schema from genkit.core.typing import ( DocumentData, GenerateActionOptions, GenerateActionOutputConfig, + GenerateRequest, GenerationCommonConfig, Message, + OutputConfig, Part, Resume, Role, @@ -61,6 +69,8 @@ Tools, ) +logger = structlog.get_logger(__name__) + class PromptCache: """Model for a prompt cache.""" @@ -121,6 +131,9 @@ def __init__( tools: list[str] | None = None, tool_choice: ToolChoice | None = None, use: list[ModelMiddleware] | None = None, + _name: str | None = None, # prompt name for action lookup + _ns: str | None = None, # namespace for action lookup + _prompt_action: Action | None = None, # reference to PROMPT action # TODO: # docs: list[Document]): ): @@ -169,6 +182,9 @@ def __init__( self._tool_choice = tool_choice self._use = use self._cache_prompt = PromptCache() + self._name = _name # Store name/ns for action lookup (used by as_tool()) + self._ns = _ns + self._prompt_action = _prompt_action async def __call__( self, @@ -318,6 +334,34 @@ async def render( resume=resume, ) + async def as_tool(self) -> Action: + """Expose this prompt as a tool. + + Returns the PROMPT action, which can be used as a tool. + """ + # If we have a direct reference to the action, use it + if self._prompt_action is not None: + return self._prompt_action + + # Otherwise, try to look it up using name/variant/ns + if self._name is None: + raise GenkitError( + status='FAILED_PRECONDITION', + message='Prompt name not available. This prompt was not created via define_prompt_async() or load_prompt().', + ) + + lookup_key = registry_lookup_key(self._name, self._variant, self._ns) + + action = self._registry.lookup_action_by_key(lookup_key) + + if action is None or action.kind != ActionKind.PROMPT: + raise GenkitError( + status='NOT_FOUND', + message=f'PROMPT action not found for prompt "{self._name}"', + ) + + return action + def define_prompt( registry: Registry, @@ -467,6 +511,59 @@ async def to_generate_action_options(registry: Registry, options: PromptConfig) ) +async def to_generate_request(registry: Registry, options: GenerateActionOptions) -> GenerateRequest: + """Converts GenerateActionOptions to a GenerateRequest. + + This function resolves tool names into their respective tool definitions + by looking them up in the provided registry. it also validates that the + provided options contain at least one message. + + Args: + registry: The Registry instance used to look up tool actions. + options: The GenerateActionOptions containing the configuration, + messages, and tool references to be converted. + + Returns: + A GenerateRequest object populated with messages, config, resolved + tools, and output configurations. + + Raises: + Exception: If a tool name provided in options cannot be found in + the registry. + GenkitError: If the options do not contain any messages. + """ + + tools: list[Action] = [] + if options.tools: + for tool_name in options.tools: + tool_action = registry.lookup_action(ActionKind.TOOL, tool_name) + if tool_action is None: + raise GenkitError(status='NOT_FOUND', message=f'Unable to resolve tool {tool_name}') + tools.append(tool_action) + + tool_defs = [to_tool_definition(tool) for tool in tools] if tools else [] + + if not options.messages: + raise GenkitError( + status='INVALID_ARGUMENT', + message='at least one message is required in generate request', + ) + + return GenerateRequest( + messages=options.messages, + config=options.config if options.config is not None else {}, + docs=options.docs, + tools=tool_defs, + tool_choice=options.tool_choice, + output=OutputConfig( + content_type=options.output.content_type if options.output else None, + format=options.output.format if options.output else None, + schema_=options.output.json_schema if options.output else None, + constrained=options.output.constrained if options.output else None, + ), + ) + + def _normalize_prompt_arg( prompt: str | Part | list[Part] | None, ) -> list[Part] | None: @@ -523,7 +620,7 @@ async def render_system_prompt( prompt_cache.system = await registry.dotprompt.compile(options.system) if options.metadata: - context = {**context, 'state': options.metadata.get('state')} + context = {**(context or {}), 'state': options.metadata.get('state')} return Message( role=Role.SYSTEM, @@ -615,7 +712,7 @@ async def render_message_prompt( prompt_cache.messages = await registry.dotprompt.compile(options.messages) if options.metadata: - context = {**context, 'state': options.metadata.get('state')} + context = {**(context or {}), 'state': options.metadata.get('state')} messages_ = None if isinstance(options.messages, list): @@ -666,7 +763,7 @@ async def render_user_prompt( prompt_cache.user_prompt = await registry.dotprompt.compile(options.prompt) if options.metadata: - context = {**context, 'state': options.metadata.get('state')} + context = {**(context or {}), 'state': options.metadata.get('state')} return Message( role=Role.USER, @@ -679,3 +776,443 @@ async def render_user_prompt( ) return Message(role=Role.USER, content=_normalize_prompt_arg(options.prompt)) + + +def registry_definition_key(name: str, variant: str | None = None, ns: str | None = None) -> str: + """Generate a registry definition key for a prompt. + + Format: "ns/name.variant" where ns and variant are optional. + + Args: + name: The prompt name. + variant: Optional variant name. + ns: Optional namespace. + + Returns: + Registry key string. + """ + parts = [] + if ns: + parts.append(ns) + parts.append(name) + if variant: + parts[-1] = f'{parts[-1]}.{variant}' + return '/'.join(parts) + + +def registry_lookup_key(name: str, variant: str | None = None, ns: str | None = None) -> str: + """Generate a registry lookup key for a prompt. + + Args: + name: The prompt name. + variant: Optional variant name. + ns: Optional namespace. + + Returns: + Registry lookup key string. + """ + return f'/prompt/{registry_definition_key(name, variant, ns)}' + + +def define_partial(registry: Registry, name: str, source: str) -> None: + """Define a partial template in the registry. + + Partials are reusable template fragments that can be included in other prompts. + Files starting with `_` are treated as partials. + + Args: + registry: The registry to register the partial in. + name: The name of the partial. + source: The template source code. + """ + registry.dotprompt.define_partial(name, source) + logger.debug(f'Registered Dotprompt partial "{name}"') + + +def define_helper(registry: Registry, name: str, fn: Callable) -> None: + """Define a Handlebars helper function in the registry. + + Args: + registry: The registry to register the helper in. + name: The name of the helper function. + fn: The helper function to register. + """ + registry.dotprompt.define_helper(name, fn) + logger.debug(f'Registered Dotprompt helper "{name}"') + + +def load_prompt(registry: Registry, path: Path, filename: str, prefix: str = '', ns: str = 'dotprompt') -> None: + """Load a single prompt file and register it in the registry. + + This function loads a .prompt file, parses it, and registers it as a lazy-loaded + prompt that will only be fully loaded when first accessed. + + Args: + registry: The registry to register the prompt in. + path: Base path to the prompts directory. + filename: Name of the prompt file (e.g., "myPrompt.prompt" or "myPrompt.variant.prompt"). + prefix: Subdirectory prefix (for namespacing). + ns: Namespace for the prompt. + """ + # Extract name and variant from filename + # "myPrompt.prompt" -> name="myPrompt", variant=None + # "myPrompt.variant.prompt" -> name="myPrompt", variant="variant" + # "subdir/myPrompt.prompt" -> name="subdir/myPrompt", variant=None + if not filename.endswith('.prompt'): + raise ValueError(f"Invalid prompt filename: {filename}. Must end with '.prompt'") + + base_name = filename.removesuffix('.prompt') + + if prefix: + name = f'{prefix}{base_name}' + else: + name = base_name + variant: str | None = None + + # Extract variant (only takes parts[1], not all remaining parts) + if '.' in name: + parts = name.split('.') + name = parts[0] + variant = parts[1] # Only first part after split + + # Build full file path + # prefix may have trailing slash, so we need to handle it + if prefix: + # Strip trailing slash for path construction (pathlib handles it) + prefix_clean = prefix.rstrip('/') + file_path = path / prefix_clean / filename + else: + file_path = path / filename + + # Read the prompt file + with open(file_path, 'r', encoding='utf-8') as f: + source = f.read() + + # Parse the prompt + parsed_prompt = registry.dotprompt.parse(source) + + # Generate registry key + registry_key = registry_definition_key(name, variant, ns) + + # Create a lazy-loaded prompt definition + # The prompt will only be fully loaded when first accessed + async def load_prompt_metadata(): + """Lazy loader for prompt metadata.""" + prompt_metadata = await registry.dotprompt.render_metadata(parsed_prompt) + + # Convert Pydantic model to dict if needed + if hasattr(prompt_metadata, 'model_dump'): + prompt_metadata_dict = prompt_metadata.model_dump() + elif hasattr(prompt_metadata, 'dict'): + prompt_metadata_dict = prompt_metadata.dict() + else: + # Already a dict + prompt_metadata_dict = prompt_metadata + + if variant: + prompt_metadata_dict['variant'] = variant + + # Clean up null descriptions + output = prompt_metadata_dict.get('output') + if output and isinstance(output, dict): + schema = output.get('schema') + if schema and isinstance(schema, dict) and schema.get('description') is None: + schema.pop('description', None) + + input_schema = prompt_metadata_dict.get('input') + if input_schema and isinstance(input_schema, dict): + schema = input_schema.get('schema') + if schema and isinstance(schema, dict) and schema.get('description') is None: + schema.pop('description', None) + + # Build metadata structure + metadata = { + **prompt_metadata_dict.get('metadata', {}), + 'type': 'prompt', + 'prompt': { + **prompt_metadata_dict, + 'template': parsed_prompt.template, + }, + } + + raw = prompt_metadata_dict.get('raw') + if raw and isinstance(raw, dict) and raw.get('metadata'): + metadata['metadata'] = {**raw['metadata']} + + output = prompt_metadata_dict.get('output') + input_schema = prompt_metadata_dict.get('input') + raw = prompt_metadata_dict.get('raw') + + return { + 'name': registry_key, + 'model': prompt_metadata_dict.get('model'), + 'config': prompt_metadata_dict.get('config'), + 'tools': prompt_metadata_dict.get('tools'), + 'description': prompt_metadata_dict.get('description'), + 'output': { + 'jsonSchema': output.get('schema') if output and isinstance(output, dict) else None, + 'format': output.get('format') if output and isinstance(output, dict) else None, + }, + 'input': { + 'jsonSchema': input_schema.get('schema') if input_schema and isinstance(input_schema, dict) else None, + }, + 'metadata': metadata, + 'maxTurns': raw.get('maxTurns') if raw and isinstance(raw, dict) else None, + 'toolChoice': raw.get('toolChoice') if raw and isinstance(raw, dict) else None, + 'returnToolRequests': raw.get('returnToolRequests') if raw and isinstance(raw, dict) else None, + 'messages': parsed_prompt.template, + } + + # Create a factory function that will create the ExecutablePrompt when accessed + # Store metadata in a closure to avoid global state + async def create_prompt_from_file(): + """Factory function to create ExecutablePrompt from file metadata.""" + metadata = await load_prompt_metadata() + + # Create ExecutablePrompt from metadata + # Pass name/ns so as_tool() can look up the action + executable_prompt = ExecutablePrompt( + registry=registry, + variant=metadata.get('variant'), + model=metadata.get('model'), + config=metadata.get('config'), + description=metadata.get('description'), + input_schema=metadata.get('input', {}).get('jsonSchema'), + output_schema=metadata.get('output', {}).get('jsonSchema'), + output_format=metadata.get('output', {}).get('format'), + messages=metadata.get('messages'), + max_turns=metadata.get('maxTurns'), + tool_choice=metadata.get('toolChoice'), + return_tool_requests=metadata.get('returnToolRequests'), + metadata=metadata.get('metadata'), + tools=metadata.get('tools'), + _name=name, # Store name for action lookup + _ns=ns, # Store namespace for action lookup + ) + + # Store reference to PROMPT action on the ExecutablePrompt + # Actions are already registered at this point (lazy loading happens after registration) + lookup_key = registry_lookup_key(name, variant, ns) + prompt_action = registry.lookup_action_by_key(lookup_key) + if prompt_action and prompt_action.kind == ActionKind.PROMPT: + executable_prompt._prompt_action = prompt_action + # Also store ExecutablePrompt reference on the action + # prompt_action._executable_prompt = executable_prompt + prompt_action._executable_prompt = weakref.ref(executable_prompt) + + return executable_prompt + + # Store the async factory in a way that can be accessed later + # We'll store it in the action metadata + action_metadata = { + 'type': 'prompt', + 'lazy': True, + 'source': 'file', + '_async_factory': create_prompt_from_file, + } + + # Create two separate action functions : + # 1. PROMPT action - returns GenerateRequest (for rendering prompts) + # 2. EXECUTABLE_PROMPT action - returns GenerateActionOptions (for executing prompts) + + async def prompt_action_fn(input: Any = None) -> GenerateRequest: + """PROMPT action function - renders prompt and returns GenerateRequest.""" + # Load the prompt (lazy loading) + prompt = await create_prompt_from_file() + + # Render the prompt with input to get GenerateActionOptions + options = await prompt.render(input=input) + + # Convert GenerateActionOptions to GenerateRequest + return await to_generate_request(registry, options) + + async def executable_prompt_action_fn(input: Any = None) -> GenerateActionOptions: + """EXECUTABLE_PROMPT action function - renders prompt and returns GenerateActionOptions.""" + # Load the prompt (lazy loading) + prompt = await create_prompt_from_file() + + # Render the prompt with input to get GenerateActionOptions + return await prompt.render(input=input) + + # Register the PROMPT action + # Use registry_definition_key for the action name (not registry_lookup_key) + # The action name should be just the definition key (e.g., "dotprompt/testPrompt"), + # not the full lookup key (e.g., "/prompt/dotprompt/testPrompt") + action_name = registry_definition_key(name, variant, ns) + prompt_action = registry.register_action( + kind=ActionKind.PROMPT, + name=action_name, + fn=prompt_action_fn, + metadata=action_metadata, + ) + + # Register the EXECUTABLE_PROMPT action + executable_prompt_action = registry.register_action( + kind=ActionKind.EXECUTABLE_PROMPT, + name=action_name, + fn=executable_prompt_action_fn, + metadata=action_metadata, + ) + + # Store the factory function on both actions for easy access + prompt_action._async_factory = create_prompt_from_file + executable_prompt_action._async_factory = create_prompt_from_file + + # Store ExecutablePrompt reference on actions + # This will be set when the prompt is first accessed (lazy loading) + # We'll update it in create_prompt_from_file after the prompt is created + + logger.debug(f'Registered prompt "{registry_key}" from "{file_path}"') + + +def load_prompt_folder_recursively(registry: Registry, dir_path: Path, ns: str, sub_dir: str = '') -> None: + """Recursively load all prompt files from a directory. + + Args: + registry: The registry to register prompts in. + dir_path: Base path to the prompts directory. + ns: Namespace for prompts. + sub_dir: Current subdirectory being processed (for recursion). + """ + full_path = dir_path / sub_dir if sub_dir else dir_path + + if not full_path.exists() or not full_path.is_dir(): + return + + # Iterate through directory entries + try: + for entry in os.scandir(full_path): + if entry.is_file() and entry.name.endswith('.prompt'): + if entry.name.startswith('_'): + # This is a partial + partial_name = entry.name[1:-7] # Remove "_" prefix and ".prompt" suffix + with open(entry.path, 'r', encoding='utf-8') as f: + source = f.read() + define_partial(registry, partial_name, source) + logger.debug(f'Registered Dotprompt partial "{partial_name}" from "{entry.path}"') + else: + # This is a regular prompt + prefix_with_slash = f'{sub_dir}/' if sub_dir else '' + load_prompt(registry, dir_path, entry.name, prefix_with_slash, ns) + elif entry.is_dir(): + # Recursively process subdirectories + new_sub_dir = os.path.join(sub_dir, entry.name) if sub_dir else entry.name + load_prompt_folder_recursively(registry, dir_path, ns, new_sub_dir) + except PermissionError: + logger.warning(f'Permission denied accessing directory: {full_path}') + except Exception as e: + logger.error(f'Error loading prompts from {full_path}: {e}') + + +def load_prompt_folder(registry: Registry, dir_path: str | Path = './prompts', ns: str = 'dotprompt') -> None: + """Load all prompt files from a directory. + + This is the main entry point for loading prompts from a directory. + It recursively processes all `.prompt` files and registers them. + + Args: + registry: The registry to register prompts in. + dir_path: Path to the prompts directory. Defaults to './prompts'. + ns: Namespace for prompts. Defaults to 'dotprompt'. + """ + path = Path(dir_path).resolve() + + if not path.exists(): + logger.warning(f'Prompt directory does not exist: {path}') + return + + if not path.is_dir(): + logger.warning(f'Prompt path is not a directory: {path}') + return + + load_prompt_folder_recursively(registry, path, ns, '') + logger.info(f'Loaded prompts from directory: {path}') + + +async def lookup_prompt(registry: Registry, name: str, variant: str | None = None) -> ExecutablePrompt: + """Look up a prompt from the registry. + + Args: + registry: The registry to look up the prompt from. + name: The name of the prompt. + variant: Optional variant name. + + Returns: + An ExecutablePrompt instance. + + Raises: + GenkitError: If the prompt is not found. + """ + # Try without namespace first (for programmatic prompts) + # Use create_action_key to build the full key: "/prompt/" + definition_key = registry_definition_key(name, variant, None) + lookup_key = create_action_key(ActionKind.PROMPT, definition_key) + action = registry.lookup_action_by_key(lookup_key) + + # If not found and no namespace was specified, try with default 'dotprompt' namespace + # (for file-based prompts) + if not action: + definition_key = registry_definition_key(name, variant, 'dotprompt') + lookup_key = create_action_key(ActionKind.PROMPT, definition_key) + action = registry.lookup_action_by_key(lookup_key) + + if action: + # First check if we've stored the ExecutablePrompt directly + if hasattr(action, '_executable_prompt') and action._executable_prompt is not None: + return action._executable_prompt + elif hasattr(action, '_async_factory'): + # Otherwise, create it from the factory (lazy loading) + # This will also set _executable_prompt on the action for future lookups + executable_prompt = await action._async_factory() + # Store it on the action for future lookups (if not already stored) + if not hasattr(action, '_executable_prompt') or action._executable_prompt is None: + action._executable_prompt = executable_prompt + return executable_prompt + else: + # Fallback: try to get from metadata + factory = action.metadata.get('_async_factory') + if factory: + executable_prompt = await factory() + # Store it on the action for future lookups + if not hasattr(action, '_executable_prompt') or action._executable_prompt is None: + action._executable_prompt = executable_prompt + return executable_prompt + # Last resort: this shouldn't happen if prompts are loaded correctly + raise GenkitError( + status='INTERNAL', + message=f'Prompt action found but no ExecutablePrompt available for {name}', + ) + + variant_str = f' (variant {variant})' if variant else '' + raise GenkitError( + status='NOT_FOUND', + message=f'Prompt {name}{variant_str} not found', + ) + + +async def prompt( + registry: Registry, + name: str, + variant: str | None = None, + dir: str | Path | None = None, # Accepted but not used +) -> ExecutablePrompt: + """Look up a prompt by name and optional variant. + + Can look up prompts that were: + 1. Defined programmatically using define_prompt() + 2. Loaded from .prompt files using load_prompt_folder() + + Args: + registry: The registry to look up the prompt from. + name: The name of the prompt. + variant: Optional variant name. + dir: Optional directory parameter (accepted for compatibility but not used). + + Returns: + An ExecutablePrompt instance. + + Raises: + GenkitError: If the prompt is not found. + """ + + return await lookup_prompt(registry, name, variant) diff --git a/py/packages/genkit/tests/genkit/blocks/prompt_test.py b/py/packages/genkit/tests/genkit/blocks/prompt_test.py index 1b62b92835..2d73f48353 100644 --- a/py/packages/genkit/tests/genkit/blocks/prompt_test.py +++ b/py/packages/genkit/tests/genkit/blocks/prompt_test.py @@ -17,14 +17,19 @@ """Tests for the action module.""" +import tempfile +from pathlib import Path from typing import Any import pytest from pydantic import BaseModel, Field from genkit.ai import Genkit +from genkit.blocks.prompt import load_prompt_folder, lookup_prompt, prompt +from genkit.core.action.types import ActionKind from genkit.core.typing import ( GenerateActionOptions, + GenerateRequest, GenerationCommonConfig, Message, Role, @@ -226,3 +231,317 @@ async def test_prompt_rendering_dotprompt( response = await my_prompt(input, input_option, context=context) assert response.text == want_rendered + + +# Tests for prompt variants and partials +@pytest.mark.asyncio +async def test_load_prompt_variant() -> None: + """Test loading and using a prompt variant.""" + ai, *_ = setup_test() + + with tempfile.TemporaryDirectory() as tmpdir: + prompt_dir = Path(tmpdir) / 'prompts' + prompt_dir.mkdir() + + # Create base prompt + base_prompt = prompt_dir / 'greeting.prompt' + base_prompt.write_text('---\nmodel: echoModel\n---\nHello {{name}}!') + + # Create variant prompt + variant_prompt = prompt_dir / 'greeting.casual.prompt' + variant_prompt.write_text("---\nmodel: echoModel\n---\nHey {{name}}, what's up?") + + load_prompt_folder(ai.registry, prompt_dir) + + # Test base prompt + base_exec = await prompt(ai.registry, 'greeting') + base_response = await base_exec({'name': 'Alice'}) + assert 'Hello' in base_response.text + assert 'Alice' in base_response.text + + # Test variant prompt + casual_exec = await prompt(ai.registry, 'greeting', variant='casual') + casual_response = await casual_exec({'name': 'Bob'}) + assert 'Hey' in casual_response.text or "what's up" in casual_response.text.lower() + assert 'Bob' in casual_response.text + + +@pytest.mark.asyncio +async def test_load_nested_prompt() -> None: + """Test loading prompts from subdirectories.""" + ai, *_ = setup_test() + + with tempfile.TemporaryDirectory() as tmpdir: + prompt_dir = Path(tmpdir) / 'prompts' + prompt_dir.mkdir() + + # Create subdirectory + sub_dir = prompt_dir / 'admin' + sub_dir.mkdir() + + # Create prompt in subdirectory + admin_prompt = sub_dir / 'dashboard.prompt' + admin_prompt.write_text('---\nmodel: echoModel\n---\nWelcome Admin {{name}}') + + load_prompt_folder(ai.registry, prompt_dir) + + # Test loading nested prompt + # Based on logic: name = "admin/dashboard" + admin_exec = await prompt(ai.registry, 'admin/dashboard') + response = await admin_exec({'name': 'SuperUser'}) + + assert 'Welcome Admin' in response.text + assert 'SuperUser' in response.text + + + +@pytest.mark.asyncio +async def test_load_and_use_partial() -> None: + """Test loading and using partials in prompts.""" + ai, *_ = setup_test() + + with tempfile.TemporaryDirectory() as tmpdir: + prompt_dir = Path(tmpdir) / 'prompts' + prompt_dir.mkdir() + + # Create partial + partial_file = prompt_dir / '_greeting.prompt' + partial_file.write_text('Hello from partial!') + + # Create prompt that uses the partial + prompt_file = prompt_dir / 'story.prompt' + prompt_file.write_text('---\nmodel: echoModel\n---\n{{>greeting}} Tell me about {{topic}}.') + + load_prompt_folder(ai.registry, prompt_dir) + + story_exec = await prompt(ai.registry, 'story') + response = await story_exec({'topic': 'space'}) + + # The partial should be included in the output + assert 'Hello from partial' in response.text or 'space' in response.text + + +@pytest.mark.asyncio +async def test_prompt_with_messages_list() -> None: + """Test prompt with explicit messages list.""" + ai, *_ = setup_test() + + messages = [ + Message(role=Role.SYSTEM, content=[TextPart(text='You are helpful')]), + Message(role=Role.USER, content=[TextPart(text='Hi there')]), + ] + + my_prompt = ai.define_prompt( + messages=messages, + prompt='How can I help?', + ) + + response = await my_prompt() + + # Should include system, user history, and final prompt + assert 'helpful' in response.text.lower() or 'Hi there' in response.text + + +@pytest.mark.asyncio +async def test_messages_with_explicit_override() -> None: + """Test that explicit messages in render options are included.""" + ai, *_ = setup_test() + + my_prompt = ai.define_prompt( + prompt='Final question', + ) + + override_messages = [ + Message(role=Role.USER, content=[TextPart(text='First message')]), + Message(role=Role.MODEL, content=[TextPart(text='First response')]), + ] + + # The override messages should be prepended to the prompt + rendered = await my_prompt.render(input=None, config=None) + + # Check that we have the final prompt message + assert any('Final question' in str(msg) for msg in rendered.messages) + + +@pytest.mark.asyncio +async def test_prompt_with_tools_list() -> None: + """Test prompt with tools parameter.""" + ai, *_ = setup_test() + + class ToolInput(BaseModel): + value: int = Field(description='A value') + + @ai.tool(name='myTool') + def my_tool(input: ToolInput): + return input.value * 2 + + my_prompt = ai.define_prompt( + prompt='Use the tool', + tools=['myTool'], + ) + + rendered = await my_prompt.render() + + # Verify tools are in the rendered options + assert rendered.tools is not None + assert 'myTool' in rendered.tools + + +@pytest.mark.asyncio +async def test_system_and_prompt_together() -> None: + """Test rendering system, messages, and prompt in correct order.""" + ai, *_ = setup_test() + + my_prompt = ai.define_prompt( + system='System instruction', + messages=[ + Message(role=Role.USER, content=[TextPart(text='History user')]), + Message(role=Role.MODEL, content=[TextPart(text='History model')]), + ], + prompt='Final prompt', + ) + + response = await my_prompt() + + # All parts should be in the response + text = response.text.lower() + assert 'system' in text or 'instruction' in text + assert 'history' in text or 'final' in text or 'prompt' in text + + +@pytest.mark.asyncio +async def test_prompt_with_output_schema() -> None: + """Test that output schema is preserved in rendering.""" + ai, *_ = setup_test() + + class OutputSchema(BaseModel): + name: str = Field(description='A name') + age: int = Field(description='An age') + + my_prompt = ai.define_prompt( + prompt='Generate a person', + output_schema=OutputSchema, + output_format='json', + ) + + rendered = await my_prompt.render() + + # Verify output configuration + assert rendered.output is not None + assert rendered.output.format == 'json' + assert rendered.output.json_schema is not None + + +@pytest.mark.asyncio +async def test_config_merge_priority() -> None: + """Test that runtime config overrides definition config.""" + ai, *_ = setup_test() + + my_prompt = ai.define_prompt( + prompt='test', + config={'temperature': 0.5, 'banana': 'yellow'}, + ) + + # Runtime config should override temperature but keep banana + rendered = await my_prompt.render(config={'temperature': 0.9}) + + assert rendered.config is not None + assert rendered.config.temperature == 0.9 + + +# Tests for file-based prompt loading and two-action structure +@pytest.mark.asyncio +async def test_file_based_prompt_registers_two_actions() -> None: + """File-based prompts create both PROMPT and EXECUTABLE_PROMPT actions.""" + ai, *_ = setup_test() + + with tempfile.TemporaryDirectory() as tmpdir: + prompt_dir = Path(tmpdir) / 'prompts' + prompt_dir.mkdir() + + # Simple prompt file: name is "filePrompt" + prompt_file = prompt_dir / 'filePrompt.prompt' + prompt_file.write_text('hello {{name}}') + + # Load prompts from directory + load_prompt_folder(ai.registry, prompt_dir) + + # Actions are registered with registry_definition_key (e.g., "dotprompt/filePrompt") + # We need to look them up by kind and name (without the /prompt/ prefix) + action_name = 'dotprompt/filePrompt' # registry_definition_key format + + prompt_action = ai.registry.lookup_action(ActionKind.PROMPT, action_name) + executable_prompt_action = ai.registry.lookup_action(ActionKind.EXECUTABLE_PROMPT, action_name) + + assert prompt_action is not None + assert executable_prompt_action is not None + + +@pytest.mark.asyncio +async def test_prompt_and_executable_prompt_return_types() -> None: + """PROMPT action returns GenerateRequest, EXECUTABLE_PROMPT returns GenerateActionOptions.""" + ai, *_ = setup_test() + + # Test with file-based prompt (which creates both actions) + # Programmatic prompts don't create actions - they're just ExecutablePrompt instances + with tempfile.TemporaryDirectory() as tmpdir: + prompt_dir = Path(tmpdir) / 'prompts' + prompt_dir.mkdir() + + prompt_file = prompt_dir / 'testPrompt.prompt' + prompt_file.write_text('hello {{name}}') + + load_prompt_folder(ai.registry, prompt_dir) + action_name = 'dotprompt/testPrompt' + + prompt_action = ai.registry.lookup_action(ActionKind.PROMPT, action_name) + executable_prompt_action = ai.registry.lookup_action(ActionKind.EXECUTABLE_PROMPT, action_name) + + assert prompt_action is not None + assert executable_prompt_action is not None + + prompt_result = await prompt_action.arun(input={'name': 'World'}) + assert isinstance(prompt_result.response, GenerateRequest) + + exec_result = await executable_prompt_action.arun(input={'name': 'World'}) + assert isinstance(exec_result.response, GenerateActionOptions) + + +@pytest.mark.asyncio +async def test_lookup_prompt_returns_executable_prompt() -> None: + """lookup_prompt should return an ExecutablePrompt that can be called.""" + + ai, *_ = setup_test() + + with tempfile.TemporaryDirectory() as tmpdir: + prompt_dir = Path(tmpdir) / 'prompts' + prompt_dir.mkdir() + + prompt_file = prompt_dir / 'lookupTest.prompt' + prompt_file.write_text('hi {{name}}') + + load_prompt_folder(ai.registry, prompt_dir) + + executable = await lookup_prompt(ai.registry, 'lookupTest') + + response = await executable({'name': 'World'}) + assert 'World' in response.text + + +@pytest.mark.asyncio +async def test_prompt_function_uses_lookup_prompt() -> None: + ai, *_ = setup_test() + + with tempfile.TemporaryDirectory() as tmpdir: + prompt_dir = Path(tmpdir) / 'prompts' + prompt_dir.mkdir() + + prompt_file = prompt_dir / 'promptFuncTest.prompt' + prompt_file.write_text('hello {{name}}') + + load_prompt_folder(ai.registry, prompt_dir) + + # Use prompt() function to look up the file-based prompt + executable = await prompt(ai.registry, 'promptFuncTest') + response = await executable({'name': 'World'}) + assert 'World' in response.text diff --git a/py/plugins/anthropic/pyproject.toml b/py/plugins/anthropic/pyproject.toml index e21fada3c8..9fbf89959c 100644 --- a/py/plugins/anthropic/pyproject.toml +++ b/py/plugins/anthropic/pyproject.toml @@ -1,3 +1,19 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + [project] authors = [{ name = "Google" }] classifiers = [ diff --git a/py/plugins/firebase/src/genkit/plugins/firebase/tests/test_telemetry.py b/py/plugins/firebase/src/genkit/plugins/firebase/tests/test_telemetry.py index d5e9955ca7..c420859ce3 100644 --- a/py/plugins/firebase/src/genkit/plugins/firebase/tests/test_telemetry.py +++ b/py/plugins/firebase/src/genkit/plugins/firebase/tests/test_telemetry.py @@ -23,8 +23,8 @@ def _create_model_span( - model_name: str = "gemini-pro", - path: str = "/{myflow,t:flow}", + model_name: str = 'gemini-pro', + path: str = '/{myflow,t:flow}', output: str = '{"usage": {"inputTokens": 100, "outputTokens": 50}}', is_ok: bool = True, start_time: int = 1000000000, @@ -45,11 +45,11 @@ def _create_model_span( """ mock_span = MagicMock(spec=ReadableSpan) mock_span.attributes = { - "genkit:type": "action", - "genkit:metadata:subtype": "model", - "genkit:name": model_name, - "genkit:path": path, - "genkit:output": output, + 'genkit:type': 'action', + 'genkit:metadata:subtype': 'model', + 'genkit:name': model_name, + 'genkit:path': path, + 'genkit:output': output, } mock_span.status.is_ok = is_ok mock_span.start_time = start_time @@ -57,18 +57,18 @@ def _create_model_span( return mock_span -@patch("genkit.plugins.firebase.add_gcp_telemetry") +@patch('genkit.plugins.firebase.add_gcp_telemetry') def test_firebase_telemetry_delegates_to_gcp(mock_add_gcp_telemetry): """Test that Firebase telemetry delegates to GCP telemetry.""" add_firebase_telemetry() mock_add_gcp_telemetry.assert_called_once_with(force_export=False) -@patch("genkit.plugins.google_cloud.telemetry.metrics._output_tokens") -@patch("genkit.plugins.google_cloud.telemetry.metrics._input_tokens") -@patch("genkit.plugins.google_cloud.telemetry.metrics._latency") -@patch("genkit.plugins.google_cloud.telemetry.metrics._failures") -@patch("genkit.plugins.google_cloud.telemetry.metrics._requests") +@patch('genkit.plugins.google_cloud.telemetry.metrics._output_tokens') +@patch('genkit.plugins.google_cloud.telemetry.metrics._input_tokens') +@patch('genkit.plugins.google_cloud.telemetry.metrics._latency') +@patch('genkit.plugins.google_cloud.telemetry.metrics._failures') +@patch('genkit.plugins.google_cloud.telemetry.metrics._requests') def test_record_generate_metrics_with_model_action( mock_requests, mock_failures, @@ -91,8 +91,8 @@ def test_record_generate_metrics_with_model_action( # Create test span using helper mock_span = _create_model_span( - model_name="gemini-pro", - path="/{myflow,t:flow}", + model_name='gemini-pro', + path='/{myflow,t:flow}', output='{"usage": {"inputTokens": 100, "outputTokens": 50}}', ) @@ -100,7 +100,7 @@ def test_record_generate_metrics_with_model_action( record_generate_metrics(mock_span) # Verify dimensions - expected_dimensions = {"model": "gemini-pro", "source": "myflow", "error": "none"} + expected_dimensions = {'model': 'gemini-pro', 'source': 'myflow', 'error': 'none'} # Verify requests counter mock_request_counter.add.assert_called_once_with(1, expected_dimensions) diff --git a/py/samples/anthropic-hello/pyproject.toml b/py/samples/anthropic-hello/pyproject.toml index 0a923fe7e5..17ec62d435 100644 --- a/py/samples/anthropic-hello/pyproject.toml +++ b/py/samples/anthropic-hello/pyproject.toml @@ -1,21 +1,37 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + [project] -name = "anthropic-hello" -version = "0.1.0" -description = "Anthropic Hello Sample" -requires-python = ">=3.10" dependencies = [ - "genkit", - "genkit-plugin-anthropic", - "pydantic>=2.0.0", - "structlog>=24.0.0", + "genkit", + "genkit-plugin-anthropic", + "pydantic>=2.0.0", + "structlog>=24.0.0", ] +description = "Anthropic Hello Sample" +name = "anthropic-hello" +requires-python = ">=3.10" +version = "0.1.0" [tool.uv.sources] genkit-plugin-anthropic = { workspace = true } [build-system] -requires = ["hatchling"] build-backend = "hatchling.build" +requires = ["hatchling"] [tool.hatch.build.targets.wheel] packages = ["src"] diff --git a/py/uv.lock b/py/uv.lock index 2cacda34ef..300a2a19eb 100644 --- a/py/uv.lock +++ b/py/uv.lock @@ -1,8 +1,9 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.13'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", "python_full_version >= '3.11' and python_full_version < '3.13'", "python_full_version < '3.11'", ] @@ -1357,6 +1358,7 @@ version = "0.4.0" source = { editable = "plugins/google-cloud" } dependencies = [ { name = "genkit" }, + { name = "opentelemetry-exporter-gcp-monitoring" }, { name = "opentelemetry-exporter-gcp-trace" }, { name = "strenum", marker = "python_full_version < '3.11'" }, ] @@ -1364,6 +1366,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "genkit", editable = "packages/genkit" }, + { name = "opentelemetry-exporter-gcp-monitoring", specifier = ">=1.9.0" }, { name = "opentelemetry-exporter-gcp-trace", specifier = ">=1.9.0" }, { name = "strenum", marker = "python_full_version < '3.11'", specifier = ">=0.4.15" }, ] @@ -1545,7 +1548,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio" }, + { name = "grpcio", version = "1.72.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "grpcio", version = "1.76.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "grpcio-status" }, ] @@ -1634,6 +1638,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/ce/3bb46615668647e3880292ef2248f39ebcc38b7d083d593d28fa94fe8ca3/google_cloud_firestore-2.20.2-py3-none-any.whl", hash = "sha256:0ff7b4c66e3ad2fe00f7d5d8c15127bf4ff8b316c6e4eb635ac51d9a9bcd828b", size = 358912, upload-time = "2025-04-17T14:30:20.128Z" }, ] +[[package]] +name = "google-cloud-monitoring" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "grpcio", version = "1.72.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "grpcio", version = "1.76.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/b8/7f68a7738cbfef610af532b2fc758e39d852fc93ed3a31bd0e76fd45d2fd/google_cloud_monitoring-2.28.0.tar.gz", hash = "sha256:25175590907e038add644b5b744941d221776342924637095a879973a7c0ac37", size = 393321, upload-time = "2025-10-14T15:42:55.786Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/d3/02dcf5376cb4b47b9c06eba36d80700d5b0a1510f3fcd47d3abbe4b0f0a3/google_cloud_monitoring-2.28.0-py3-none-any.whl", hash = "sha256:64f4c57cc465dd51cceffe559f0ec6fa9f96aa6d82790cd8d3af6d5cc3795160", size = 384670, upload-time = "2025-10-14T15:42:41.911Z" }, +] + [[package]] name = "google-cloud-resource-manager" version = "1.14.2" @@ -1881,7 +1902,8 @@ wheels = [ [package.optional-dependencies] grpc = [ - { name = "grpcio" }, + { name = "grpcio", version = "1.72.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "grpcio", version = "1.76.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, ] [[package]] @@ -1942,7 +1964,8 @@ version = "0.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos", extra = ["grpc"] }, - { name = "grpcio" }, + { name = "grpcio", version = "1.72.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "grpcio", version = "1.76.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/4e/8d0ca3b035e41fe0b3f31ebbb638356af720335e5a11154c330169b40777/grpc_google_iam_v1-0.14.2.tar.gz", hash = "sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20", size = 16259, upload-time = "2025-03-17T11:40:23.586Z" } @@ -1954,6 +1977,11 @@ wheels = [ name = "grpcio" version = "1.72.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*'", + "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version < '3.11'", +] sdist = { url = "https://files.pythonhosted.org/packages/fe/45/ff8c80a5a2e7e520d9c4d3c41484a11d33508253f6f4dd06d2c4b4158999/grpcio-1.72.1.tar.gz", hash = "sha256:87f62c94a40947cec1a0f91f95f5ba0aa8f799f23a1d42ae5be667b6b27b959c", size = 12584286, upload-time = "2025-06-02T10:14:11.595Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5a/a8/a468586ef3db8cd90f507c0e5655c50cdf136e936f674effddacd5e6f83b/grpcio-1.72.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:ce2706ff37be7a6de68fbc4c3f8dde247cab48cc70fee5fedfbc9cd923b4ee5a", size = 5210592, upload-time = "2025-06-02T10:08:34.416Z" }, @@ -1998,13 +2026,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/10/b6186e92eba035315affc30dfeabf65594dd6f778b92627fae5f40e7beec/grpcio-1.72.1-cp313-cp313-win_amd64.whl", hash = "sha256:329cc6ff5b431df9614340d3825b066a1ff0a5809a01ba2e976ef48c65a0490b", size = 4221454, upload-time = "2025-06-02T10:10:16.73Z" }, ] +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, + { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + [[package]] name = "grpcio-status" version = "1.72.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, - { name = "grpcio" }, + { name = "grpcio", version = "1.72.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "grpcio", version = "1.76.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/b8/e563262a30065d3b52b61ca92c427fe2a1b04ba5dfca0415ae0df8ecdac8/grpcio_status-1.72.1.tar.gz", hash = "sha256:627111a87afa920eafb42cc6c50db209d263e07fa51fbb084981ef636566be7b", size = 13646, upload-time = "2025-06-02T10:14:23.666Z" } @@ -2159,7 +2252,8 @@ name = "ipython" version = "9.0.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", "python_full_version >= '3.11' and python_full_version < '3.13'", ] dependencies = [ @@ -3391,6 +3485,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/44/4c45a34def3506122ae61ad684139f0bbc4e00c39555d4f7e20e0e001c8a/opentelemetry_api-1.33.1-py3-none-any.whl", hash = "sha256:4db83ebcf7ea93e64637ec6ee6fabee45c5cbe4abd9cf3da95c43828ddb50b83", size = 65771, upload-time = "2025-05-16T18:52:17.419Z" }, ] +[[package]] +name = "opentelemetry-exporter-gcp-monitoring" +version = "1.11.0a0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-cloud-monitoring" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-resourcedetector-gcp" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/48/d1c7d2380bb1754d1eb6a011a2e0de08c6868cb6c0f34bcda0444fa0d614/opentelemetry_exporter_gcp_monitoring-1.11.0a0.tar.gz", hash = "sha256:386276eddbbd978a6f30fafd3397975beeb02a1302bdad554185242a8e2c343c", size = 20828, upload-time = "2025-11-04T19:32:14.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/8c/03a6e73e270a9c890dbd6cc1c47c83d86b8a8a974a9168d92e043c6277cc/opentelemetry_exporter_gcp_monitoring-1.11.0a0-py3-none-any.whl", hash = "sha256:b6740cba61b2f9555274829fe87a58447b64d0378f1067a4faebb4f5b364ca22", size = 13611, upload-time = "2025-11-04T19:32:08.212Z" }, +] + [[package]] name = "opentelemetry-exporter-gcp-trace" version = "1.9.0"