showcase/shell-docs/src/content/docs/integrations/langgraph/shared-state/state-inputs-outputs.mdx
Not all state properties are relevant for frontend-backend sharing. This guide shows how to ensure only the right portion of state is communicated back and forth.
This guide is based on LangGraph's Input/Output Schema feature
Depending on your implementation, some properties are meant to be processed internally, while some others are the way for the UI to communicate user input. In addition, some state properties contain a lot of information. Syncing them back and forth between the agent and UI can be costly, while it might not have any practical benefit.
class AgentState(CopilotKitState):
question: str
answer: str
resources: List[str]
```
</Tab>
<Tab value="TypeScript">
```typescript title="agent-js/sample_agent/agent.ts"
import { Annotation } from "@langchain/langgraph";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
import { z } from "zod";
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
question: Annotation<string>,
answer: Annotation<string>,
resources: Annotation<string[]>,
});
export type AgentState = typeof AgentStateAnnotation.State;
```
</Tab>
</Tabs>
<Tabs groupId="language_langgraph_agent" items={['Python', 'TypeScript']} default="Python" persist>
<Tab value="Python">
```python title="agent.py"
from langchain_core.runnables import RunnableConfig
from copilotkit import CopilotKitState
from typing import Literal
# Divide the state to 3 parts
# Input schema for inputs you are willing to accept from the frontend
class InputState(CopilotKitState):
question: str
# Output schema for output you are willing to pass to the frontend
class OutputState(CopilotKitState):
answer: str
# The full schema, including the inputs, outputs and internal state ("resources" in our case)
class OverallState(InputState, OutputState):
resources: List[str]
async def answer_node(state: OverallState, config: RunnableConfig):
"""
Standard chat node, meant to answer general questions.
"""
model = ChatOpenAI()
# add the input question in the system prompt so it's passed to the LLM
system_message = SystemMessage(
content=f"You are a helpful assistant. Answer the question: {state.get('question')}"
)
response = await model.ainvoke([
system_message,
*state["messages"],
], config)
# ...add the rest of the agent implementation
# extract the answer, which will be assigned to the state soon
answer = response.content
return {
"messages": response,
# include the answer in the returned state
"answer": answer
}
# finally, before compiling the graph, we define the 3 state components
builder = StateGraph(OverallState, input=InputState, output=OutputState)
# add all the different nodes and edges and compile the graph
builder.add_node("answer_node", answer_node)
builder.add_edge(START, "answer_node")
builder.add_edge("answer_node", END)
graph = builder.compile()
```
</Tab>
<Tab value="TypeScript">
```typescript title="agent-js/sample_agent/agent.ts"
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";
import { CopilotKitStateAnnotation } from "@copilotkit/sdk-js/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { SystemMessage } from "@langchain/core/messages";
import type { RunnableConfig } from "@langchain/core/runnables";
// Divide the state to 3 parts
// An input annotation for inputs you are willing to accept from the frontend
const InputAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
question: Annotation<string>,
});
// Output annotation for output you are willing to pass to the frontend
const OutputAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
answer: Annotation<string>,
});
// The full annotation, including the inputs, outputs and internal state ("resources" in our case)
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
question: Annotation<string>,
answer: Annotation<string>,
resources: Annotation<string[]>,
});
export type AgentState = typeof AgentStateAnnotation.State;
async function answerNode(state: AgentState, config: RunnableConfig) {
const model = new ChatOpenAI();
const systemMessage = new SystemMessage({
content: `You are a helpful assistant. Answer the question: ${state.question}.`,
});
const response = await model.invoke(
[systemMessage, ...state.messages],
config
);
// ...add the rest of the agent implementation
// extract the answer, which will be assigned to the state soon
const answer = typeof response.content === 'string'
? response.content
: JSON.stringify(response.content);
return {
messages: [response],
// include the answer in the returned state
answer,
}
}
// finally, before compiling the graph, we define the 3 state components
// StateGraph accepts the full state annotation as the first parameter,
// with optional input/output annotations to filter what's communicated with the frontend
const workflow = new StateGraph(AgentStateAnnotation, {
input: InputAnnotation,
output: OutputAnnotation,
})
.addNode("answer_node", answerNode)
.addEdge(START, "answer_node")
.addEdge("answer_node", END);
export const graph = workflow.compile();
```
</Tab>
</Tabs>
```tsx
import { useAgent } from "@copilotkit/react-core/v2"; // [!code highlight]
const { agent } = useAgent({
agentId: "sample_agent",
});
const answer = agent.state.answer as string;
console.log(answer) // You can expect seeing "answer" change, while the others are not returned from the agent
```