Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions py/packages/genkit/src/genkit/ai/_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,19 +686,47 @@ async def prompt(

def define_resource(
self,
opts: 'ResourceOptions',
fn: 'ResourceFn',
opts: 'ResourceOptions | None' = None,
fn: 'ResourceFn | None' = None,
*,
name: str | None = None,
uri: str | None = None,
template: str | None = None,
description: str | None = None,
metadata: dict[str, Any] | None = None,
) -> Action:
"""Define a resource action.

Args:
opts: Options defining the resource (e.g. uri, template, name).
fn: Function implementing the resource behavior.
name: Optional name for the resource.
uri: Optional URI for the resource.
template: Optional URI template for the resource.
description: Optional description for the resource.
metadata: Optional metadata for the resource.

Returns:
The registered Action for the resource.
"""
from genkit.blocks.resource import define_resource as define_resource_block
from genkit.blocks.resource import (
define_resource as define_resource_block,
)

if fn is None:
raise ValueError("A function `fn` must be provided to define a resource.")
if opts is None:
opts = {}
if name:
opts['name'] = name
if uri:
opts['uri'] = uri
if template:
opts['template'] = template
if description:
opts['description'] = description
if metadata:
opts['metadata'] = metadata

return define_resource_block(self.registry, opts, fn)

Expand Down
2 changes: 1 addition & 1 deletion py/packages/genkit/src/genkit/blocks/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -794,7 +794,7 @@ async def render_message_prompt(
resolved = await ensure_async(options.messages)(input, context)
return resolved

raise TypeError(f"Unsupported type for messages: {type(options.messages)}")
raise TypeError(f'Unsupported type for messages: {type(options.messages)}')


async def render_user_prompt(
Expand Down
43 changes: 37 additions & 6 deletions py/packages/genkit/src/genkit/blocks/resource.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 Google LLC
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -21,12 +21,14 @@
and return content (`ResourceOutput`) containing `Part`s.
"""

import inspect
import re
from collections.abc import Awaitable, Callable
from typing import Any, Callable, Protocol, TypedDict
from typing import Any, Protocol, TypedDict

from pydantic import BaseModel

from genkit.aio import ensure_async
from genkit.core.action import Action, ActionRunContext
from genkit.core.action.types import ActionKind
from genkit.core.registry import Registry
Expand All @@ -35,6 +37,7 @@

class ResourceOptions(TypedDict, total=False):
"""Options for defining a resource.

Attributes:
name: Resource name. If not specified, uri or template will be used as name.
uri: The URI of the resource. Can contain template variables for simple matches,
Expand All @@ -54,6 +57,7 @@ class ResourceOptions(TypedDict, total=False):

class ResourceInput(BaseModel):
"""Input structure for a resource request.

Attributes:
uri: The full URI being requested/resolved.
"""
Expand All @@ -63,6 +67,7 @@ class ResourceInput(BaseModel):

class ResourceOutput(BaseModel):
"""Output structure from a resource resolution.

Attributes:
content: A list of `Part` objects representing the resource content.
"""
Expand All @@ -84,12 +89,15 @@ def __call__(self, input: ResourceInput, ctx: ActionRunContext) -> Awaitable[Res

async def resolve_resources(registry: Registry, resources: list[ResourceArgument] | None = None) -> list[Action]:
"""Resolves a list of resource names or actions into a list of Action objects.

Args:
registry: The registry to lookup resources in.
resources: A list of resource references, which can be either direct `Action`
objects or strings (names/URIs).

Returns:
A list of resolved `Action` objects.

Raises:
ValueError: If a resource reference is invalid or cannot be found.
"""
Expand All @@ -111,11 +119,14 @@ async def lookup_resource_by_name(registry: Registry, name: str) -> Action:
"""Looks up a resource action by name in the registry.
Tries to resolve the name directly, or with common prefixes like `/resource/`
or `/dynamic-action-provider/`.

Args:
registry: The registry to search.
name: The name or URI of the resource to lookup.

Returns:
The found `Action`.

Raises:
ValueError: If the resource cannot be found.
"""
Expand All @@ -133,10 +144,12 @@ def define_resource(registry: Registry, opts: ResourceOptions, fn: ResourceFn) -
"""Defines a resource and registers it with the given registry.
This creates a resource action that can handle requests for a specific URI
or URI template.

Args:
registry: The registry to register the resource with.
opts: Options defining the resource (name, uri, template, etc.).
fn: The function that implements resource content retrieval.

Returns:
The registered `Action` for the resource.
"""
Expand All @@ -156,9 +169,11 @@ def resource(opts: ResourceOptions, fn: ResourceFn) -> Action:
"""Defines a dynamic resource action without immediate registration.
This is an alias for `dynamic_resource`. Useful for defining resources that
might be registered later or used as standalone actions.

Args:
opts: Options defining the resource.
fn: The resource implementation function.

Returns:
The created `Action`.
"""
Expand All @@ -172,11 +187,14 @@ def dynamic_resource(opts: ResourceOptions, fn: ResourceFn) -> Action:
1. Input validation and matching against the URI/Template.
2. Execution of the resource function.
3. Post-processing of output to attach metadata (like parent resource info).

Args:
opts: Options including `uri` or `template` for matching.
fn: The function performing the resource retrieval.

Returns:
An `Action` configured as a resource.

Raises:
ValueError: If neither `uri` nor `template` is provided in options.
"""
Expand All @@ -195,7 +213,16 @@ async def wrapped_fn(input_data: ResourceInput, ctx: ActionRunContext) -> Resour
if not template_match:
raise ValueError(f'input {input_data} did not match template {uri}')

parts = await fn(input_data, ctx)
sig = inspect.signature(fn)
afn = ensure_async(fn)
n_params = len(sig.parameters)

if n_params == 0:
parts = await afn()
elif n_params == 1:
parts = await afn(input_data)
else:
parts = await afn(input_data, ctx)

# Post-processing parts to add metadata
content_list = parts.content if hasattr(parts, 'content') else parts.get('content', [])
Expand Down Expand Up @@ -237,7 +264,7 @@ async def wrapped_fn(input_data: ResourceInput, ctx: ActionRunContext) -> Resour
parts['content'] = [p.model_dump() if isinstance(p, BaseModel) else p for p in parts['content']]
return parts
return parts
except Exception as e:
except Exception:
raise

name = opts.get('name') or uri
Expand All @@ -262,9 +289,11 @@ async def wrapped_fn(input_data: ResourceInput, ctx: ActionRunContext) -> Resour

def create_matcher(uri: str | None, template: str | None) -> Callable[[ResourceInput], bool]:
"""Creates a matching function for resource validation.

Args:
uri: Optional fixed URI string.
template: Optional URI template string.

Returns:
A callable that takes ResourceInput and returns True if it matches.
"""
Expand All @@ -281,8 +310,10 @@ def matcher(input_data: ResourceInput) -> bool:

def is_dynamic_resource_action(action: Action) -> bool:
"""Checks if an action is a dynamic resource (not registered).

Args:
action: The action to check.

Returns:
True if the action is a dynamic resource, False otherwise.
"""
Expand Down Expand Up @@ -334,10 +365,12 @@ async def find_matching_resource(
) -> Action | None:
"""Finds a matching resource action.
Checks dynamic resources first, then the registry.

Args:
registry: The registry to search.
dynamic_resources: Optional list of dynamic resource actions to check first.
input_data: The resource input containing the URI matched against.

Returns:
The matching Action or None.
"""
Expand All @@ -351,8 +384,6 @@ async def find_matching_resource(
if resource:
return resource

# Iterate all resources to check for matches (e.g. templates)
# This is less efficient but necessary for template matching if not optimized
# Iterate all resources to check for matches (e.g. templates)
# This is less efficient but necessary for template matching if not optimized
resources = registry.get_actions_by_kind(ActionKind.RESOURCE) if hasattr(registry, 'get_actions_by_kind') else {}
Expand Down
2 changes: 1 addition & 1 deletion py/packages/genkit/tests/genkit/veneer/resource_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 Google LLC
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion py/packages/genkit/tests/genkit/veneer/veneer_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
# Copyright 2025 Google LLC
# Copyright 2026 Google LLC
# SPDX-License-Identifier: Apache-2.0

"""Tests for the action module."""
Expand Down
3 changes: 3 additions & 0 deletions py/plugins/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Genkit MCP Plugin

Integrate Model Context Protocol (MCP) with Genkit.
53 changes: 53 additions & 0 deletions py/plugins/mcp/examples/client/simple_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# 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

import asyncio

from genkit.ai import Genkit
from genkit.plugins.mcp import McpServerConfig, create_mcp_client

try:
from genkit.plugins.google_genai import GoogleAI
except ImportError:
GoogleAI = None


# Simple client example connecting to 'everything' server using npx
async def main():
# Define the client plugin
everything_client = create_mcp_client(
name='everything', config=McpServerConfig(command='npx', args=['-y', '@modelcontextprotocol/server-everything'])
)

plugins = [everything_client]
if GoogleAI:
plugins.append(GoogleAI())

ai = Genkit(plugins=plugins)

await everything_client.connect()

print('Connected! Listing tools...')

tools = await everything_client.list_tools()
for t in tools:
print(f'- {t.name}: {t.description}')

await everything_client.close()


if __name__ == '__main__':
asyncio.run(main())
13 changes: 13 additions & 0 deletions py/plugins/mcp/examples/server/prompts/port_code.prompt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
input:
schema:
code: string, the source code to port from one language to another
fromLang?: string, the original language of the source code (e.g. js, python)
toLang: string, the destination language of the source code (e.g. python, js)
---

You are assisting the user in translating code between two programming languages. Given the code below, translate it into {{toLang}}.

```{{#if fromLang}}{{fromLang}}{{/if}}
{{code}}
```
63 changes: 63 additions & 0 deletions py/plugins/mcp/examples/server/simple_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2026 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

import asyncio

from pydantic import BaseModel, Field

from genkit.ai import Genkit
from genkit.plugins.mcp import McpServerOptions, create_mcp_server


# Define input model
class AddInput(BaseModel):
a: int = Field(..., description='First number')
b: int = Field(..., description='Second number')


import os


def main():
# Load prompts from the 'prompts' directory relative to this script
script_dir = os.path.dirname(os.path.abspath(__file__))
prompts_dir = os.path.join(script_dir, 'prompts')

ai = Genkit(prompt_dir=prompts_dir)

@ai.tool(name='add', description='add two numbers together')
def add(input: AddInput):
return input.a + input.b

# Genkit Python prompt definition (simplified)
# Note: In Python, prompts are typically loaded from files via prompt_dir
# This inline definition is for demonstration purposes
happy_prompt = ai.define_prompt(
input_schema={'action': str},
prompt="If you're happy and you know it, {{action}}.",
)

# Create and start MCP server
# Note: create_mcp_server returns McpServer instance.
# In JS example: .start() is called.
server = create_mcp_server(ai, McpServerOptions(name='example_server', version='0.0.1'))

print('Starting MCP server on stdio...')
asyncio.run(server.start())


if __name__ == '__main__':
main()
Loading
Loading