Demo
Although you’ve worked on the project in the two previous lessons, today, you’ll start from scratch based on the architectural design you created.
Make sure that you’ve set your OPENAI_API_KEY in the .env file. Then load it:
from dotenv import load_dotenv
load_dotenv()
Find save_file.png and save_file.yaml in the root of the project Starter folder. You’ll need to install a package to read the YAML file:
pip install pyyaml
Give your code access to the files by writing a couple functions:
import yaml
from base64 import b64encode
def read_yaml_file(file_path):
with open(file_path, 'r') as file:
yaml_content = yaml.safe_load(file)
return yaml.dump(yaml_content, default_flow_style=False)
def encode_image(image_path):
with open(image_path, 'rb') as image_file:
return b64encode(image_file.read()).decode('utf-8')
app_strings = read_yaml_file('save_file.yaml')
screenshot = encode_image('save_file.png')
The text strings are stored in YAML format. Setting default_flow_style to False maintains the line breaks. The image is a PNG, but the LLM needs it to be stored as a Base64 string. That’s a way of storing binary data in string format.
Import the rest of the libraries you’ll need. Then, create a graph state class:
from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
import operator
import os
from langchain_openai import ChatOpenAI
class State(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
translation_count: int
translation: str
contextualized: str
advice: str
In addition to messages, your state class has a few more properties that you’ll use during the workflow. translation_count will count how many times the original text gets translated so that you don’t get into an infinite loop with an overeager translation checker agent that keeps sending it back. translation will hold the current translation. contextualized will hold the commented version of the original text. advice will hold any translation advice from the checker.
Set up your LLM:
llm = ChatOpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
model="gpt-4o"
)
Add a function to add comments to the app strings based on the UI screenshot:
def contextualize(state):
print("contextualizing")
prompt = """You are an expert in mobile app string localization
and internationalization. You are preparing app strings to be
localized in another language by providing additional
context in English to help the translator. Add comments to
each line of the following text based on what you see in
the image. Use YAML style comments and put them on the
line above the text being commented."""
user = HumanMessage(content=[
{"type": "text", "text": prompt},
{"type": "text", "text": app_strings},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{screenshot}"
}
}
])
state["messages"].append(user)
response = llm.invoke([user])
return {"messages": [response], "contextualized": response.content}
This will be the backing function for the Contextualizer node. You’re using OpenAI’s multi-modal support for images in addition to text. After you get the contextualized text, you save it to the state.
Add another function for the Translation node:
def translate(state):
print("translating")
given_text = state["contextualized"]
prompt = f"""You are a world-class translator. Translate the given text
from English to Spanish. Each line is commented and you should take
those comments into consideration in order to get an accurate translation.
Don't translate the comments or the keys. Given text:
{given_text}
"""
advice = state["advice"]
if advice:
prompt = prompt + f"Here is some advice to follow when
translating: {advice}"
user = HumanMessage(content=prompt)
state["messages"].append(user)
response = llm.invoke([user])
return {"messages": [response],
"translation_count": state["translation_count"] + 1,
"translation": response.content
}
Since this node may be called for both initial translation and subsequent revisions, you’re leaving a place to add some advice.
Add another function to check the translation. This will be the backing function for the Checker node:
def check(state):
print("checking")
translation = state["translation"]
prompt = f"""You are an expert in mobile app UI/UX and also
cross-cultural communication. A translator has submitted a
translation for the strings of a UI layout. Check the
translation for accuracy. If the translation is good, reply with one
word: "done". If not, provide some helpful advice for improving the
translation. Just use a bulleted list of points to pay attention to.
Here is the original text:
{app_strings}
Here is the translation with contextual comments:
{translation}
The YAML comments and keys didn't need to be translated.
"""
user = HumanMessage(content=prompt)
state["messages"].append(user)
response = llm.invoke([user])
return {"messages": [response], "advice": response.content}
You could’ve made translate a tool and bound it to the LLM, but the workflow here uses a simple "done" response to determine the control flow. You’ll handle that next.
def should_stop_translating(state):
translation_count = state["translation_count"]
if translation_count > 2:
return True
ai_response = state["messages"][-1].content.lower()
return ai_response == "done"
If the translation count goes above two or the AI has deemed the translation “done”, this function will signal to stop the translation-checking loop.
The final task of the agent workflow is to format the output. Add a function for that:
def format_translation(state):
print("formatting")
translation = state["translation"]
prompt = f"""Clean this YAML text up by removing any comments.
Don't make any comments:
{translation}
"""
user = HumanMessage(content=prompt)
state["messages"].append(user)
response = llm.invoke([user])
return {"messages": [response]}
Later, you’ll modify this method to put the output in a different format. A simple cleanup is good enough for now.
Now, you’re ready to build the graph:
graph = StateGraph(State)
graph.add_node("contextualizer", contextualize)
graph.add_edge(START, "contextualizer")
graph.add_node("translator", translate)
graph.add_edge("contextualizer", "translator")
graph.add_node("checker", check)
graph.add_edge("translator", "checker")
graph.add_node("formatter", format_translation)
graph.add_conditional_edges(
"checker",
should_stop_translating,
{
True: "formatter",
False: "translator"
}
)
graph.add_edge("formatter", END)
app = graph.compile()
There are five nodes besides the built-in START and END nodes. Most of them have normal edges, but there are conditional edges between the checker and the translator and formatter nodes. should_stop_translating is the function that determines the route and when the looping ends.
Take a look at the visual representation of the graph:
from IPython.display import Image, display
display(Image(app.get_graph().draw_mermaid_png()))
The final step is to run the compiled graph:
state = State(
messages=[],
translation_count=0,
contextualized="",
translation="",
advice=""
)
response = app.invoke(state)
print(response['messages'][-1].content)
print(response)
The app runs the steps one at a time and then prints the final translated text:
message: ¿Le gustaría guardar el archivo?
negative_button: Descartar
positive_button: Guardar
title: Pregunta
How is this? You’ll need to check it with a Spanish speaker if you don’t know Spanish yourself. Thankfully, Fernando, the tech editor for this module, is a native Spanish speaker and can help out.
Before you go, scroll down through all the messages in the response output. Look for the AIMessage and HumanMessage objects to break the conversation into chunks. All those numbers and letters are the Base64-encoded screenshot image. Notice the flow that the agent took. In this case, it appears that the checker accepted the translation on the first try.