Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a0032cf
feat(go): Add support for genkit resources
huangjeff5 Jul 29, 2025
5f10992
add copyright
huangjeff5 Jul 29, 2025
53faf28
Add action type resource
huangjeff5 Jul 29, 2025
9c4b6cd
move helpers to core
huangjeff5 Jul 29, 2025
0ebba79
remove unused code
huangjeff5 Jul 29, 2025
fa392d0
Update generate.go
huangjeff5 Jul 30, 2025
58c93cf
Update generate.go
huangjeff5 Jul 30, 2025
b08729d
Update resource.go
huangjeff5 Jul 30, 2025
0f23cab
dynamic handlers don't need cleanup
huangjeff5 Jul 30, 2025
388b7ef
fix cleanup
huangjeff5 Jul 30, 2025
b761633
Update generate.go
huangjeff5 Jul 30, 2025
623be6c
Update generate.go
huangjeff5 Jul 30, 2025
5f78159
Update generate.go
huangjeff5 Jul 30, 2025
7694bf4
Update generate.go
huangjeff5 Jul 30, 2025
d9193ce
Change AttachToRegistry to Register
huangjeff5 Jul 30, 2025
99a55ba
Merge branch 'jh-go-resources' of https://github.com/firebase/genkit …
huangjeff5 Jul 30, 2025
93af415
Address comments
huangjeff5 Aug 15, 2025
adcef6a
format
huangjeff5 Aug 15, 2025
dc0f83c
fix
huangjeff5 Aug 15, 2025
ed1b300
Auto-gen
huangjeff5 Aug 15, 2025
064c269
fix
huangjeff5 Aug 15, 2025
1699e8a
refactor
huangjeff5 Aug 15, 2025
0c610f3
Merge branch 'main' into jh-go-resources
huangjeff5 Aug 15, 2025
ac9241d
regen
huangjeff5 Aug 15, 2025
218084f
move Resource to commongenoptions
huangjeff5 Aug 15, 2025
6abdbc5
refactor
huangjeff5 Aug 16, 2025
0863d90
Update go/ai/resource.go
huangjeff5 Aug 19, 2025
ba2a329
Update go/ai/resource.go
huangjeff5 Aug 19, 2025
26c5301
Update go/genkit/resource.go
huangjeff5 Aug 19, 2025
8947f8c
Update go/genkit/resource.go
huangjeff5 Aug 19, 2025
77a7ce4
Update go/genkit/resource.go
huangjeff5 Aug 19, 2025
e642717
Update go/genkit/resource.go
huangjeff5 Aug 19, 2025
0dfbf6f
Update go/genkit/resource.go
huangjeff5 Aug 19, 2025
ce8e2fc
Update go/genkit/resource.go
huangjeff5 Aug 19, 2025
daa26c7
refactor
huangjeff5 Aug 19, 2025
b8d969b
move everything into genkit.go
huangjeff5 Aug 19, 2025
772ca9c
fix
huangjeff5 Aug 19, 2025
c773627
Merge branch 'main' into jh-go-resources
huangjeff5 Aug 19, 2025
372d6eb
fix
huangjeff5 Aug 19, 2025
9aae9f0
fix
huangjeff5 Aug 19, 2025
cf1a0bf
fix
huangjeff5 Aug 20, 2025
178c0e9
Update go/genkit/genkit.go
huangjeff5 Aug 21, 2025
0d74a41
fix
huangjeff5 Aug 21, 2025
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
22 changes: 22 additions & 0 deletions go/ai/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Part struct {
Text string `json:"text,omitempty"` // valid for kind∈{text,blob}
ToolRequest *ToolRequest `json:"toolRequest,omitempty"` // valid for kind==partToolRequest
ToolResponse *ToolResponse `json:"toolResponse,omitempty"` // valid for kind==partToolResponse
Resource *ResourcePart `json:"resource,omitempty"` // valid for kind==partResource
Custom map[string]any `json:"custom,omitempty"` // valid for plugin-specific custom parts
Metadata map[string]any `json:"metadata,omitempty"` // valid for all kinds
}
Expand All @@ -52,6 +53,7 @@ const (
PartToolResponse
PartCustom
PartReasoning
PartResource
)

// NewTextPart returns a Part containing text.
Expand Down Expand Up @@ -118,6 +120,11 @@ func NewReasoningPart(text string, signature []byte) *Part {
}
}

// NewResourcePart returns a Part containing a resource reference.
func NewResourcePart(uri string) *Part {
return &Part{Kind: PartResource, Resource: &ResourcePart{Uri: uri}}
}

// IsText reports whether the [Part] contains plain text.
func (p *Part) IsText() bool {
return p.Kind == PartText
Expand Down Expand Up @@ -153,6 +160,11 @@ func (p *Part) IsReasoning() bool {
return p.Kind == PartReasoning
}

// IsResource reports whether the [Part] contains a resource reference.
func (p *Part) IsResource() bool {
return p.Kind == PartResource
}

// MarshalJSON is called by the JSON marshaler to write out a Part.
func (p *Part) MarshalJSON() ([]byte, error) {
// This is not handled by the schema generator because
Expand Down Expand Up @@ -192,6 +204,12 @@ func (p *Part) MarshalJSON() ([]byte, error) {
Metadata: p.Metadata,
}
return json.Marshal(v)
case PartResource:
v := resourcePart{
Resource: p.Resource,
Metadata: p.Metadata,
}
return json.Marshal(v)
case PartCustom:
v := customPart{
Custom: p.Custom,
Expand All @@ -215,6 +233,7 @@ type partSchema struct {
Data string `json:"data,omitempty" yaml:"data,omitempty"`
ToolRequest *ToolRequest `json:"toolRequest,omitempty" yaml:"toolRequest,omitempty"`
ToolResponse *ToolResponse `json:"toolResponse,omitempty" yaml:"toolResponse,omitempty"`
Resource *ResourcePart `json:"resource,omitempty" yaml:"resource,omitempty"`
Custom map[string]any `json:"custom,omitempty" yaml:"custom,omitempty"`
Metadata map[string]any `json:"metadata,omitempty" yaml:"metadata,omitempty"`
Reasoning string `json:"reasoning,omitempty" yaml:"reasoning,omitempty"`
Expand All @@ -233,6 +252,9 @@ func (p *Part) unmarshalPartFromSchema(s partSchema) {
case s.ToolResponse != nil:
p.Kind = PartToolResponse
p.ToolResponse = s.ToolResponse
case s.Resource != nil:
p.Kind = PartResource
p.Resource = s.Resource
case s.Custom != nil:
p.Kind = PartCustom
p.Custom = s.Custom
Expand Down
10 changes: 10 additions & 0 deletions go/ai/gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ type GenerateActionOptions struct {
Output *GenerateActionOutputConfig `json:"output,omitempty"`
Resume *GenerateActionResume `json:"resume,omitempty"`
ReturnToolRequests bool `json:"returnToolRequests,omitempty"`
StepName string `json:"stepName,omitempty"`
ToolChoice ToolChoice `json:"toolChoice,omitempty"`
Tools []string `json:"tools,omitempty"`
}
Expand Down Expand Up @@ -315,6 +316,15 @@ type RerankerResponse struct {
Documents []*RankedDocumentData `json:"documents,omitempty"`
}

type resourcePart struct {
Metadata map[string]any `json:"metadata,omitempty"`
Resource *ResourcePart `json:"resource,omitempty"`
}

type ResourcePart struct {
Uri string `json:"uri,omitempty"`
}

type RetrieverRequest struct {
Options any `json:"options,omitempty"`
Query *Document `json:"query,omitempty"`
Expand Down
100 changes: 100 additions & 0 deletions go/ai/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,15 @@ func Generate(ctx context.Context, r *registry.Registry, opts ...GenerateOption)
}
}

if len(genOpts.Resources) > 0 {
r = r.NewChild()

// Attach resources
for _, res := range genOpts.Resources {
res.Register(r)
}
}

messages := []*Message{}
if genOpts.SystemFn != nil {
system, err := genOpts.SystemFn(ctx, nil)
Expand Down Expand Up @@ -467,6 +476,13 @@ func Generate(ctx context.Context, r *registry.Registry, opts ...GenerateOption)
}
}

// Process resources in messages
processedMessages, err := processResources(ctx, r, messages)
if err != nil {
return nil, core.NewError(core.INTERNAL, "ai.Generate: error processing resources: %v", err)
}
actionOpts.Messages = processedMessages

return GenerateWithRequest(ctx, r, actionOpts, genOpts.Middleware, genOpts.Stream)
}

Expand Down Expand Up @@ -1048,3 +1064,87 @@ func handleResumeOption(ctx context.Context, r *registry.Registry, genOpts *Gene
toolMessage: toolMessage,
}, nil
}

// processResources processes messages to replace resource parts with actual content.
func processResources(ctx context.Context, r *registry.Registry, messages []*Message) ([]*Message, error) {
processedMessages := make([]*Message, len(messages))
for i, msg := range messages {
processedContent := []*Part{}

for _, part := range msg.Content {
if part.IsResource() {
// Find and execute the matching resource
resourceParts, err := executeResourcePart(ctx, r, part.Resource.Uri)
if err != nil {
return nil, fmt.Errorf("failed to process resource %q: %w", part.Resource, err)
}
// Replace resource part with content parts
processedContent = append(processedContent, resourceParts...)
} else {
// Keep non-resource parts as-is
processedContent = append(processedContent, part)
}
}

processedMessages[i] = &Message{
Role: msg.Role,
Content: processedContent,
Metadata: msg.Metadata,
}
}

return processedMessages, nil
}

// findMatchingResource finds a resource action in the registry that matches the given URI.
func findMatchingResource(r *registry.Registry, uri string) (core.Action, map[string]string, error) {
// Use our updated FindMatchingResource function
resource, resourceInput, err := FindMatchingResource(r, uri)
if err != nil {
return nil, nil, err
}

// Execute the resource to get the action - we need to access the underlying action
// Since the resource interface doesn't expose the action directly, we'll look it up
action := r.LookupAction(fmt.Sprintf("/resource/%s", resource.Name()))
if action == nil {
return nil, nil, core.NewError(core.INTERNAL, "failed to lookup resource action")
}

if coreAction, ok := action.(core.Action); ok {
return coreAction, resourceInput.Variables, nil
}

return nil, nil, core.NewError(core.INTERNAL, "action does not implement core.Action interface")
}

// executeResourcePart finds and executes a resource, returning the content parts.
func executeResourcePart(ctx context.Context, r *registry.Registry, resourceURI string) ([]*Part, error) {
action, variables, err := findMatchingResource(r, resourceURI)
if err != nil {
return nil, err
}

// Create resource input with extracted variables
input := &ResourceInput{
URI: resourceURI,
Variables: variables,
}

// Execute the resource action directly
inputJSON, _ := json.Marshal(input)
outputJSON, err := action.RunJSON(ctx, inputJSON, nil)
if err != nil {
return nil, fmt.Errorf("failed to execute resource %q: %w", resourceURI, err)
}

// Parse resource output - use a compatible structure
var output struct {
Content []*Part `json:"content"`
}
if err := json.Unmarshal(outputJSON, &output); err != nil {
return nil, fmt.Errorf("failed to parse resource output: %w", err)
}

return output.Content, nil
}
68 changes: 68 additions & 0 deletions go/ai/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1081,3 +1081,71 @@ func TestToolInterruptsAndResume(t *testing.T) {
}
})
}

func TestResourceProcessing(t *testing.T) {
r := registry.New()

// Create test resources using DefineResource
DefineResource(r, "test-file", &ResourceOptions{
URI: "file:///test.txt",
Description: "Test file resource",
}, func(ctx context.Context, input *ResourceInput) (*ResourceOutput, error) {
return &ResourceOutput{Content: []*Part{NewTextPart("FILE CONTENT")}}, nil
})

DefineResource(r, "test-api", &ResourceOptions{
URI: "api://data/123",
Description: "Test API resource",
}, func(ctx context.Context, input *ResourceInput) (*ResourceOutput, error) {
return &ResourceOutput{Content: []*Part{NewTextPart("API DATA")}}, nil
})

// Test message with resources
messages := []*Message{
NewUserMessage(
NewTextPart("Read this:"),
NewResourcePart("file:///test.txt"),
NewTextPart("And this:"),
NewResourcePart("api://data/123"),
NewTextPart("Done."),
),
}

// Process resources
processed, err := processResources(context.Background(), r, messages)
if err != nil {
t.Fatalf("resource processing failed: %v", err)
}

// Verify content
content := processed[0].Content
expected := []string{"Read this:", "FILE CONTENT", "And this:", "API DATA", "Done."}

if len(content) != len(expected) {
t.Fatalf("expected %d parts, got %d", len(expected), len(content))
}

for i, want := range expected {
if content[i].Text != want {
t.Fatalf("part %d: got %q, want %q", i, content[i].Text, want)
}
}
}

func TestResourceProcessingError(t *testing.T) {
r := registry.New()

// No resources registered
messages := []*Message{
NewUserMessage(NewResourcePart("missing://resource")),
}

_, err := processResources(context.Background(), r, messages)
if err == nil {
t.Fatal("expected error when no resources available")
}

if !strings.Contains(err.Error(), "no resource found for URI") {
t.Fatalf("wrong error: %v", err)
}
}
36 changes: 36 additions & 0 deletions go/ai/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ type commonGenOptions struct {
Model ModelArg // Model to use.
MessagesFn MessagesFn // Function to generate messages.
Tools []ToolRef // References to tools to use.
Resources []Resource // Resources to be temporarily available during generation.
ToolChoice ToolChoice // Whether tool calls are required, disabled, or optional.
MaxTurns int // Maximum number of tool call iterations.
ReturnToolRequests *bool // Whether to return tool requests instead of making the tool calls and continuing the generation.
Expand Down Expand Up @@ -147,6 +148,13 @@ func (o *commonGenOptions) applyCommonGen(opts *commonGenOptions) error {
opts.Tools = o.Tools
}

if o.Resources != nil {
if opts.Resources != nil {
return errors.New("cannot set resources more than once (WithResources)")
}
opts.Resources = o.Resources
}

if o.ToolChoice != "" {
if opts.ToolChoice != "" {
return errors.New("cannot set tool choice more than once (WithToolChoice)")
Expand Down Expand Up @@ -816,6 +824,34 @@ func WithToolRestarts(parts ...*Part) GenerateOption {
return &generateOptions{RestartParts: parts}
}

// WithResources specifies resources to be temporarily available during generation.
// Resources are unregistered resources that get attached to a temporary registry
// during the generation request and cleaned up afterward.
func WithResources(resources ...Resource) CommonGenOption {
return &withResources{resources: resources}
}

type withResources struct {
resources []Resource
}

func (w *withResources) applyCommonGen(o *commonGenOptions) error {
o.Resources = w.resources
return nil
}

func (w *withResources) applyPrompt(o *promptOptions) error {
return w.applyCommonGen(&o.commonGenOptions)
}

func (w *withResources) applyGenerate(o *generateOptions) error {
return w.applyCommonGen(&o.commonGenOptions)
}

func (w *withResources) applyPromptExecute(o *promptExecutionOptions) error {
return w.applyCommonGen(&o.commonGenOptions)
}

// promptExecutionOptions are options for generating a model response by executing a prompt.
type promptExecutionOptions struct {
commonGenOptions
Expand Down
Loading
Loading