In this demo, you’ll add memory, structured output, and human-in-the-loop interaction to the localizer project that you’ve been working on. Open the localizer.ipynb notebook in the Starter folder. You’ll find it in the same state as you left it at the end of the last lesson.
Start by running all the cells to make sure the project is still working. Because of the variable nature of LLMs, you may not get the same result as you got last time or the same result as this demo. The project is still working, though. Note that the word for “Save” is “Guardar”.
Go back to the top and start working your way down. Change the name save_file.yaml to save_puppy.yaml. Also change save_file.png to save_puppy.png:
app_strings = read_yaml_file('save_puppy.yaml')
screenshot = encode_image('save_puppy.png')
Rerun the graph. Interesting. The translation step ran twice. So, what translation for “save” did the AI Agent choose? This time it’s “Salvar”.
Go to the next cell and remove translation_count from the State class. Also, put contextualized before translation since that comes first:
class State(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
contextualized: str
translation: str
advice: str
Go to the translate function. Remove the advice parts. You’ll give the advice to a human instead of this agent. Also, remove translation_count from the return statement.
Update the prompt for the check function:
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: "good". However, if
there is anything that is ambiguous or might be translated wrong,
reply with a paragraph expressing your concern.
{app_strings}
Here is the translation with contextual comments:
{translation}
"""
Rename should_stop_translating to translation_good and change the body to:
def translation_good(state):
advice = state["advice"]
print(f"translation: \n{state["translation"]}\n")
print(f"advice: {advice}")
if advice == "good":
return "good"
else:
return "problem"
Add a new cell below that for the human_review function:
def human_review(state):
return state
Update the format_translation function so that it generates structured output. Although you may want localization files for both Android and iOS, today, you’ll focus just on iOS.
prompt = f"""The text below is a translation in YAML format.
The first step is to remove the comments.
The second step is to convert it to format the key-value pairs
in a .strings file for iOS.
Don't change the key names or the translated text at all.
Don't make any commentary. Just give the output.
Here is the text:
{translation}
"""
Add a new node for human review:
graph.add_node("human_review", human_review)
graph.add_edge("human_review", "formatter")
Update the conditional edges:
graph.add_conditional_edges(
"checker",
translation_good,
{
"good": "formatter",
"problem": "human_review"
}
)
Update the graph.compile line to include a checkpointer and an interrupt before the human review:
from langgraph.checkpoint.memory import MemorySaver
memory = MemorySaver()
app = graph.compile(checkpointer=memory, interrupt_before=["human_review"])
Check the display again. Now the checker either goes directly to the formatter or the human_review.
Remove the translation count from the State. Then add a thread to the invoke call:
thread = {"configurable": {"thread_id": "1"}}
response = app.invoke(state, thread)
Run the app and wait for the output. Hmm, the checker agent has some advice:
The translation for the "positive_button" as "Guardar" may cause confusion.
"Guardar" typically means "to save" in the context of storing or keeping
something, like a file, rather than rescuing or saving a life. A more
appropriate translation that conveys the idea of rescuing the puppy would
be "Salvar." This would ensure the intended meaning is clear to users.
Add a new cell to give the human an opportunity to override the translation based on the advice:
user_input = input("Update any line that needs changing: ")
Run that, and when prompted, type:
positive_button: Salvar
Next, you’ll use a regular expression for some text manipulation.
Extract the positive_button key from the user input. The regular expression pattern is a raw formatted string. It starts at the beginning of a line, looks for the key, and matches everything else up to the end of the line. Then, substitute that line with the new line from the user input. You need the MULTILINE flag so the regex doesn’t treat the whole translation string as a single line.
import re
key = user_input.split(":")[0]
pattern = rf"^{key}:.*$"
translation = app.get_state(thread).values["translation"]
updated = re.sub(pattern, user_input, translation, flags=re.MULTILINE)
Then, update the state with the new translation and restart the graph execution from the breakpoint by invoking the app with None for the state.
app.update_state(thread, {"translation": updated})
result = app.invoke(None, thread)
formatted = app.get_state(thread).values["messages"][-1].content
print(formatted)
Run that to see the formatted result. So, in the end, it turns out the human-in-the-loop result gave the same output as the translator-check loop version did earlier. The advantage with the human is that if the meaning should have been “Guardar”, the human could have changed the content to reflect that meaning.