-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathreact.py
More file actions
86 lines (65 loc) · 3.4 KB
/
Copy pathreact.py
File metadata and controls
86 lines (65 loc) · 3.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
"""
ReAct — LangGraph variant.
Pattern: ReAct (reason → act → observe loop with tools).
Framework: LangGraph (>=0.3.21) with langchain-anthropic for the model.
Idioms: create_react_agent() prebuilt from langgraph.prebuilt wraps the
agent loop; tools are plain Python callables registered with the agent;
LangGraph handles state, tool dispatch, and termination.
Design doc: ../../../design.md (the framework-agnostic _reference.py at
../../_reference.py shows the loop control flow without a real LLM).
Install: uv add langgraph langchain-anthropic
Run: ANTHROPIC_API_KEY=... uv run --with langgraph --with langchain-anthropic react.py
LangGraph's create_react_agent ships a pre-built ReAct loop. You supply a
model and a list of tools; the framework wires up the message graph
(model → tool → model → …) and emits a final AIMessage when the agent
stops. Contrast with the Pydantic AI sibling at ../pydantic-ai/react.py
where the loop is implicit inside agent.run_sync().
"""
from __future__ import annotations
import os
import sys
from langchain_anthropic import ChatAnthropic
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
# The prebuilt React agent owns its own message-graph state shape, so
# this adapter doesn't construct an explicit ``ReActState``. The import
# anchors the contract — recipes targeting ReAct still bind against
# ``Observation`` / ``ReActStep`` / ``ReActState`` even when the
# framework hides them.
from patterns.react.schemas.state import Observation, ReActState, ReActStep, ToolCall # noqa: F401
_MOCK_DICTIONARY: dict[str, str] = {
"recursion": "A method of solving a problem where the solution depends on solutions to smaller instances of the same problem.",
"monad": "A design pattern in functional programming that wraps values to chain operations while handling side effects.",
"agent": "An autonomous program that perceives its environment through inputs and acts on it through tools.",
}
@tool
def lookup_definition(word: str) -> str:
"""Return the canonical definition of ``word`` from the mock dictionary.
Use this exactly once per question; do not guess if it returns 'unknown'.
"""
return _MOCK_DICTIONARY.get(word.lower(), f"unknown: no entry for {word!r}")
SYSTEM_PROMPT = (
"You are a dictionary agent. Given a word, call lookup_definition exactly "
"once and then answer with the returned meaning. If the tool returns "
"'unknown', say so plainly instead of guessing."
)
def build_agent(): # type: ignore[no-untyped-def]
"""Construct the prebuilt ReAct agent.
Wrapped in a function so the import of ChatAnthropic doesn't trigger
config validation at module import (handy for tests that import this
module without an API key).
"""
model = ChatAnthropic(model_name="claude-haiku-4-5", timeout=60, stop=None)
return create_react_agent(model, tools=[lookup_definition], prompt=SYSTEM_PROMPT)
def main() -> int:
if "ANTHROPIC_API_KEY" not in os.environ:
print("Skipping smoke run — set ANTHROPIC_API_KEY to exercise the real loop.")
return 0
agent = build_agent()
result = agent.invoke({"messages": [("user", "What does the word 'recursion' mean?")]})
final = result["messages"][-1]
print(f"answer: {final.content}")
print(f"(loop produced {len(result['messages'])} messages)")
return 0
if __name__ == "__main__":
sys.exit(main())