C3 AI Documentation Home

GenAI Agent Resources

Overview

What is a Genai.Agent.Resource?

A resource is anything an agent can produce, understand, and represent in a UI.

Examples of resources include: forms, visualizations, generated documents, and even files submitted by users as part of their queries.

The following resources are available out of the box in the application:

You can quickly create any new resource using the resource framework by following the steps outlined below.

Resource lifecycle at a glance

Agent Resource lifecycle

Resources bridge the C3 Type System with the native domains, both for the server (Python) and the UI (SDL React). As a result, they implement methods to handle serialization and de-serialization across these domains:

  • toNative: Convert the C3 resource instance into a native domain object used by your agent/tool.
  • fromNative: Convert the native object back into a Genai.Agent.Resource (persisted through merge/upsert).
  • toUi (implemented in js-rhino): Describe how to render the resource using an SDL-connected React component.
  • fromUi: Apply user edits from the UI back onto the resource.

Example: Text resource

A simple text resource demonstrating how lifecycle methods work.

toNative(this)
  • Purpose: Convert the resource into a native (Python) object for agents/tools to consume.
  • Why: Agents shouldn't depend on C3 types; they operate on plain, serializable objects.
  • What it does:
    • Ensures the required fields are loaded.
    • Returns a small native object containing only what the agent needs.
Python
TextResource = c3.Genai.Agent.Resource.Text.nativePyCls()

def toNative(this):
    this = this.getMissing({"include": "text"})
    return TextResource(text=this.text)
fromNative(cls, obj)
  • Purpose: Convert a native object back into a persisted Genai.Agent.Resource.
  • Why: After tool execution, you often want to surface results to the user and to subsequent agent steps.
  • What it does:
    • Maps fields from the native object to the resource.
    • Persists only the intended fields using merge(..., returnInclude='this') or upsert.
Python
def fromNative(cls, obj):
    return c3.Genai.Agent.Resource.Text(text=obj.text).upsert(returnInclude="this")
toUi(this)
  • Purpose: Describe how the resource should be rendered in the UI.
  • Why: The UI renders based on UiSdlDynamicComponentMetadata, which points to a React component.
  • Important: the implementation must be in js-rhino for improved performance (bringing up an ephemeral Python process introduces unnecessary latency in the UI)
  • What it does:
    • Loads all required fields.
    • Returns a UiSdlConnected<GenAiUiTextResource> with minimal props (title, text, resourceId).
JavaScript
function toUi(this) {
    this = this.getMissing({include: "id, title, text"});
    return c3.UiSdlDynamicComponentMetadata({
        children: [
            {
                type: "UiSdlConnected<GenAiUiTextResource>",
                id: "GenAiUiTextResource_" + this.id,
                component: {
                    title: this.title,
                    text: this.text,
                    resourceId: this.id,
                },
            }
        ]
    });
}
fromUi(this, uiKwargs)
  • Purpose: Apply user edits coming from the UI and persist them on the resource.
  • Why: Enables incremental edit flows; the next agent run can consume updated values through toNative.
  • What it does:
    • Reads deltas from uiKwargs.
    • Merges only changed fields using merge(..., returnInclude='this').
Python
def fromUi(this, uiKwargs):
    if not uiKwargs:
        return this
    return this.withText(uiKwargs["text"]).merge(mergeInclude="text", returnInclude="this")
UI component (React) wiring for onChange

You can define your own component for rendering a GenAI Resource. It's important that this component is a C3 Type UI component, so that it can be rendered dynamically.

Below is an example of a UI component that renders a resource to edit some text. All resources have access to a GenAiUiResourceContext which contains:

  • conversationId: id of the conversation
  • feedApplicationStateId: id of the feed application state.
  • resultId: id of the GenAI result the resource belongs to.
TypeScript
import React, { useState, useContext } from 'react';
import { useDispatch } from 'react-redux';
import { onChange } from '@c3/ui/GenAiUiResourceHelpers';
import { GenAiUiResourceContext, GenAiUiResourceContextValue } from '@c3/app/ui/src/genai-react/GenAiUiCanvasResource';

function GenAiUiTextResource({ title, text, resourceId }: { title?: string; text?: string; resourceId: string }) {
  const dispatch = useDispatch();
  const [value, setValue] = useState(text);
  const { conversationId, feedApplicationStateId }: GenAiUiResourceContextValue = useContext(GenAiUiResourceContext);

  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    const newText = e.target.value;
    setValue(newText);
    onChange({
      dispatch,
      feedApplicationStateId,
      resource: { id: resourceId, text: newText, title },
      conversationId,
    });
  };

  return <textarea value={value} onChange={handleChange} />;
}
  • What onChange does: It dispatches addResourcesAction with GenAiUiResourceType.AGENT_RESOURCE_EDIT_VALUES, updating the feed app state. Later, the server-side fromUi reads uiKwargs to persist changes.
TypeScript
import { GenAiUiResourceType, addResourcesAction } from '@c3/ui/GenAiUiFeedResources';
import { Genai, GenAiUiResourceOnChangeSpec } from '@c3/types';

const onChange = (spec: GenAiUiResourceOnChangeSpec) => {
  spec.dispatch(
    addResourcesAction(
      spec.feedApplicationStateId,
      { [spec.resource.id]: spec.resource },
      GenAiUiResourceType.AGENT_RESOURCE_EDIT_VALUES,
      spec.conversationId
    )
  );
};

Step-by-step: Build a custom resource

Follow these steps to create a new resource end-to-end.

1) Define your data shape and type

  • Decide what the resource represents (for example: note, chart, form, document).
  • If content is large or versioned, plan to store it in the FileSystem and reference through contentUrl.
  • Extend Genai.Agent.Resource (in .c3typ) if you need additional fields beyond the base.

2) Implement toNative(this)

  • Return a native object that your agent/tool consumes (for example: Python class, Document, or a Python data class).

For custom Python classes, you need to include a custom __reduce__ method that removes C3 builtin references to make objects pickleable across different runtime contexts without C3 dependencies. An example is provided below.

Python
# Define this class within `nativePyCls` within your resource c3typ
class MyNative(BaseModel):
    """
    A basic native class with pickle support for serialization/deserialization.
    """
    value: Any = Field(
        title="Value",
        description="The main value stored in this native object."
    )
    metadata: Dict[str, Any] = Field(
        default_factory=dict,
        title="Metadata",
        description="Additional metadata for the object."
    )

    def __reduce__(self):
        """
        Custom pickle reduction method.
        This method is called when the object is being pickled.
        """
        import types

        def _restore_my_native(*initialization_args):
            """
            Function to restore the object during unpickling.
            This searches for 'c3' in the call stack to get the C3 context.
            """
            import sys

            # Search for c3 in the call stack frames
            try:
                c3 = None
                frame_index = 0
                while c3 is None:
                    c3 = sys._getframe(frame_index).f_globals.get("c3")
                    frame_index += 1
            except ValueError as e:
                raise RuntimeError(
                    "Failed to unpickle this instance as 'c3' is not found in runtime context."
                ) from e

            # Return a new instance using C3's native object creation
            return c3.Genai.Agent.Resource.MyNative.nativePyObj(*initialization_args)

        # Create a restore function with clean globals (no c3 references)
        restore_function = types.FunctionType(
            _restore_my_native.__code__,
            {"__builtins__": __builtins__},
        )

        # Return the restore function and initialization arguments
        initialization_args = (self.value,)
        instance_state = {
            "metadata": self.metadata,
            "value": self.value
        }
        return restore_function, initialization_args, instance_state

    def __setstate__(self, state: Dict[str, Any]):
        """
        Custom pickle state setting method.
        This method is called when the object is being unpickled.
        """
        # Restore the instance state
        self.metadata = state.get("metadata", {})
        self.value = state.get("value")


def nativePyCls(cls):
    return MyNative

def nativePyObj(cls, *args, **kwargs):
    return cls.nativePyCls()(*args, **kwargs)

def toNative(this):
    this = this.getMissing(include="value")
    return MyNative(value=this.value)

3) Implement fromNative(cls, obj)

  • Convert a native result back into the resource instance.
  • If content is large, write it to a file and set contentUrl.
  • Persist with merge(..., returnInclude='this') (or upsert).
Python
def fromNative(cls, obj):
    # If small, store inline; if large, write to FileSystem
    return cls(value=obj.value).merge(mergeInclude="value", returnInclude="this")

4) Implement toUi(this)

  • Return UiSdlDynamicComponentMetadata that references your React component name through UiSdlConnected<YourComponent>.
  • Pass only the props required for rendering.
Python
def toUi(this):
    this = this.getMissing(include="id, value")
    return c3.UiSdlDynamicComponentMetadata(
        children=[
            {
                "type": "UiSdlConnected<GenAiUiMyResource>",
                "id": f"GenAiUiMyResource_{this.id}",
                "component": {
                    "value": this.value,
                    "resourceId": this.id,
                },
            }
        ]
    )

5) Implement fromUi(this, uiKwargs)

  • Apply partial updates from the UI and persist only the changed fields.
Python
def fromUi(this, uiKwargs):
    if not uiKwargs:
        return this
    return this.withValue(uiKwargs.get("value")).merge(mergeInclude="value", returnInclude="this")

6) Choose the viewMode

  • Default is 'feed' (inline).
  • Set 'canvas' when you want the UI to transition and focus on a larger editor/visualization.
Python
# Example inside fromNative
return cls(..., viewMode="canvas").merge(mergeInclude="..., viewMode", returnInclude="this")

7) Track diffs and user actions (optional)

  • Provide computeDiff on the server and store results in lastDiff.
  • Expose userActions(this) to summarize changes for audit/history.
  • Add user actions for resources where users can take actions and you want to convey the same to the agent. For example, submitting forms, editing documents, sending emails, etc.
Python
def userActions(this):
    return [
        c3.Genai.Agent.ResourceAction(
            description="User changed this resource.",
            details="Details about the resource",
        )
    ]

8) Add your resource to the Factory mapping

  • Register the native→resource mapping so the platform can convert your native objects into Genai.Agent.Resource automatically.
  • Remix the type Genai.Agent.Resource.Factory as follows:
Text
remix type Genai.Agent.Resource.Factory {
  nativeResourceToTypeMappings: ~ py
}
Python
def nativeResourceToTypeMappings(cls):
    resource_mappings = []
    try:
        MyNative = c3.Genai.Agent.Resource.MyNative.nativePyCls()
        resource_mappings.append({"cls": MyNative, "resource": c3.Genai.Agent.Resource.MyNative})
    except ImportError:
        pass
    return resource_mappings

9) Validate with tests

You can use Genai.Agent.Dynamic.Test to easily write end-to-end tests for your new resource. You must seed a resource of the type you have created, and reference it through Genai.Agent.Dynamic.Test#queryToResourceListMapping

Polling for live updates

If your resource’s data is produced asynchronously (for example: background research), add a poll(this) function and have toUi decode it.

Example: Research run summaries

  • Writes research critic summaries to a file referenced by contentUrl.
  • poll(this) reads and returns the JSON string; toUi parses and passes to the component.
Python
def toUi(this):
    this = this.getMissing({"include": "id, filename, contentUrl, metadata"})
    thoughts = json.loads(poll(this))
    return c3.UiSdlDynamicComponentMetadata(
        children=[
            {
                "type": "UiSdlConnected<GenAiUiThoughtSummary>",
                "id": "GenAiUiThoughtSummary_" + this.id,
                "component": {
                    "thoughts": thoughts,
                    "resourceId": this.id,
                    "status": this.get("status").status,
                },
            }
        ]
    )

def poll(this):
    this = this.getMissing(include="contentUrl")
    return c3.FileSystem.makeFile(this.contentUrl).readString()

Troubleshooting / FAQ

  • My resource doesn’t show up in Canvas: Ensure viewMode is 'canvas' when returning the resource, and the component name matches in UiSdlConnected<...>.
  • Edits don’t persist: Verify your component calls onChange(...) with the correct resource.id, and fromUi merges the corresponding fields.
  • Content not updating after background work: Implement poll(this) and have toUi read and parse the latest data (for example: from contentUrl).
  • Diff is always empty: Make sure you compute against the previously persisted content (see computeDiff usage in Document).
Was this page helpful?