Skip to content

Commit afd214b

Browse files
authored
Merge pull request #154 from SentienceAPI/pydantic_ai
Phase 0: prepare for integration PydanticAI and Langchain
2 parents 05e1d8e + ecc8504 commit afd214b

29 files changed

+2156
-368
lines changed

examples/lang-chain/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### LangChain / LangGraph examples (Python)
2+
3+
These examples show how to use Sentience as a **tool layer** inside LangChain and LangGraph.
4+
5+
Install:
6+
7+
```bash
8+
pip install sentienceapi[langchain]
9+
```
10+
11+
Examples:
12+
- `langchain_tools_demo.py`: build a Sentience tool pack for LangChain
13+
- `langgraph_self_correcting_graph.py`: observe → act → verify → branch (retry) template
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""
2+
Example: Build Sentience LangChain tools (async-only).
3+
4+
Install:
5+
pip install sentienceapi[langchain]
6+
7+
Run:
8+
python examples/lang-chain/langchain_tools_demo.py
9+
10+
Notes:
11+
- This example focuses on creating the tools. Hook them into your agent of choice.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import asyncio
17+
18+
from sentience import AsyncSentienceBrowser
19+
from sentience.integrations.langchain import (
20+
SentienceLangChainContext,
21+
build_sentience_langchain_tools,
22+
)
23+
24+
25+
async def main() -> None:
26+
browser = AsyncSentienceBrowser(headless=False)
27+
await browser.start()
28+
await browser.goto("https://example.com")
29+
30+
ctx = SentienceLangChainContext(browser=browser)
31+
tools = build_sentience_langchain_tools(ctx)
32+
33+
print("Registered tools:")
34+
for t in tools:
35+
print(f"- {t.name}")
36+
37+
await browser.close()
38+
39+
40+
if __name__ == "__main__":
41+
asyncio.run(main())
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
LangGraph reference example: Sentience observe → act → verify → branch (self-correcting).
3+
4+
Install:
5+
pip install sentienceapi[langchain]
6+
7+
Run:
8+
python examples/lang-chain/langgraph_self_correcting_graph.py
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import asyncio
14+
from dataclasses import dataclass
15+
16+
from sentience import AsyncSentienceBrowser
17+
from sentience.integrations.langchain import SentienceLangChainContext, SentienceLangChainCore
18+
19+
20+
@dataclass
21+
class State:
22+
url: str | None = None
23+
last_action: str | None = None
24+
attempts: int = 0
25+
done: bool = False
26+
27+
28+
async def main() -> None:
29+
from langgraph.graph import END, StateGraph
30+
31+
browser = AsyncSentienceBrowser(headless=False)
32+
await browser.start()
33+
34+
core = SentienceLangChainCore(SentienceLangChainContext(browser=browser))
35+
36+
async def observe(state: State) -> State:
37+
s = await core.snapshot_state()
38+
state.url = s.url
39+
return state
40+
41+
async def act(state: State) -> State:
42+
# Replace with an LLM decision node. For demo we just navigate once.
43+
if state.attempts == 0:
44+
await core.navigate("https://example.com")
45+
state.last_action = "navigate"
46+
else:
47+
state.last_action = "noop"
48+
state.attempts += 1
49+
return state
50+
51+
async def verify(state: State) -> State:
52+
out = await core.verify_url_matches(r"example\.com")
53+
state.done = bool(out.passed)
54+
return state
55+
56+
def branch(state: State) -> str:
57+
if state.done:
58+
return "done"
59+
if state.attempts >= 3:
60+
return "done"
61+
return "retry"
62+
63+
g = StateGraph(State)
64+
g.add_node("observe", observe)
65+
g.add_node("act", act)
66+
g.add_node("verify", verify)
67+
g.set_entry_point("observe")
68+
g.add_edge("observe", "act")
69+
g.add_edge("act", "verify")
70+
g.add_conditional_edges("verify", branch, {"retry": "observe", "done": END})
71+
app = g.compile()
72+
73+
final = await app.ainvoke(State())
74+
print(final)
75+
76+
await browser.close()
77+
78+
79+
if __name__ == "__main__":
80+
asyncio.run(main())
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""
2+
LangGraph reference example: Sentience observe → act → verify → branch (self-correcting).
3+
4+
Install:
5+
pip install sentienceapi[langchain]
6+
7+
Run:
8+
python examples/langgraph/sentience_self_correcting_graph.py
9+
10+
Notes:
11+
- This is a template demonstrating control flow; you can replace the "decide" node
12+
with an LLM step (LangChain) that chooses actions based on snapshot_state/read_page.
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import asyncio
18+
from dataclasses import dataclass
19+
from typing import Optional
20+
21+
from sentience import AsyncSentienceBrowser
22+
from sentience.integrations.langchain import SentienceLangChainContext, SentienceLangChainCore
23+
24+
25+
@dataclass
26+
class State:
27+
url: str | None = None
28+
last_action: str | None = None
29+
attempts: int = 0
30+
done: bool = False
31+
32+
33+
async def main() -> None:
34+
# Lazy import so the file can exist without langgraph installed
35+
from langgraph.graph import END, StateGraph
36+
37+
browser = AsyncSentienceBrowser(headless=False)
38+
await browser.start()
39+
40+
core = SentienceLangChainCore(SentienceLangChainContext(browser=browser))
41+
42+
async def observe(state: State) -> State:
43+
s = await core.snapshot_state()
44+
state.url = s.url
45+
return state
46+
47+
async def act(state: State) -> State:
48+
# Replace this with an LLM-driven decision. For demo purposes, we just navigate once.
49+
if state.attempts == 0:
50+
await core.navigate("https://example.com")
51+
state.last_action = "navigate"
52+
else:
53+
state.last_action = "noop"
54+
state.attempts += 1
55+
return state
56+
57+
async def verify(state: State) -> State:
58+
# Guard condition: URL should contain example.com
59+
out = await core.verify_url_matches(r"example\.com")
60+
state.done = bool(out.passed)
61+
return state
62+
63+
def should_continue(state: State) -> str:
64+
# Self-correcting loop: retry observe→act→verify up to 3 attempts
65+
if state.done:
66+
return "done"
67+
if state.attempts >= 3:
68+
return "done"
69+
return "retry"
70+
71+
g = StateGraph(State)
72+
g.add_node("observe", observe)
73+
g.add_node("act", act)
74+
g.add_node("verify", verify)
75+
g.set_entry_point("observe")
76+
g.add_edge("observe", "act")
77+
g.add_edge("act", "verify")
78+
g.add_conditional_edges("verify", should_continue, {"retry": "observe", "done": END})
79+
app = g.compile()
80+
81+
final = await app.ainvoke(State())
82+
print(final)
83+
84+
await browser.close()
85+
86+
87+
if __name__ == "__main__":
88+
asyncio.run(main())
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
Example: PydanticAI + Sentience self-correcting action loop using URL guards.
3+
4+
Run:
5+
pip install sentienceapi[pydanticai]
6+
python examples/pydantic_ai/pydantic_ai_self_correcting_click.py
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from sentience import AsyncSentienceBrowser
12+
from sentience.integrations.pydanticai import SentiencePydanticDeps, register_sentience_tools
13+
14+
15+
async def main() -> None:
16+
from pydantic_ai import Agent
17+
18+
browser = AsyncSentienceBrowser(headless=False)
19+
await browser.start()
20+
await browser.page.goto("https://example.com") # replace with a real target
21+
22+
agent = Agent(
23+
"openai:gpt-5",
24+
deps_type=SentiencePydanticDeps,
25+
output_type=str,
26+
instructions=(
27+
"Navigate on the site and click the appropriate link/button. "
28+
"After clicking, use assert_eventually_url_matches to confirm the URL changed as expected."
29+
),
30+
)
31+
register_sentience_tools(agent)
32+
33+
deps = SentiencePydanticDeps(browser=browser)
34+
result = await agent.run("Click something that navigates, then confirm URL changed.", deps=deps)
35+
print(result.output)
36+
37+
await browser.close()
38+
39+
40+
if __name__ == "__main__":
41+
import asyncio
42+
43+
asyncio.run(main())
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
Example: PydanticAI + Sentience typed extraction (Phase 1 integration).
3+
4+
Run:
5+
pip install sentienceapi[pydanticai]
6+
python examples/pydantic_ai/pydantic_ai_typed_extraction.py
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from pydantic import BaseModel, Field
12+
13+
from sentience import AsyncSentienceBrowser
14+
from sentience.integrations.pydanticai import SentiencePydanticDeps, register_sentience_tools
15+
16+
17+
class ProductInfo(BaseModel):
18+
title: str = Field(..., description="Product title")
19+
price: str = Field(..., description="Displayed price string")
20+
21+
22+
async def main() -> None:
23+
from pydantic_ai import Agent
24+
25+
browser = AsyncSentienceBrowser(headless=False)
26+
await browser.start()
27+
await browser.page.goto("https://example.com") # replace with a real target
28+
29+
agent = Agent(
30+
"openai:gpt-5",
31+
deps_type=SentiencePydanticDeps,
32+
output_type=ProductInfo,
33+
instructions="Extract the product title and price from the page.",
34+
)
35+
register_sentience_tools(agent)
36+
37+
deps = SentiencePydanticDeps(browser=browser)
38+
result = await agent.run("Extract title and price.", deps=deps)
39+
print(result.output)
40+
41+
await browser.close()
42+
43+
44+
if __name__ == "__main__":
45+
import asyncio
46+
47+
asyncio.run(main())

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@ sentience = "sentience.cli:main"
4343
browser-use = [
4444
"browser-use>=0.1.40",
4545
]
46+
pydanticai = [
47+
# PydanticAI framework (PyPI: pydantic-ai, import: pydantic_ai)
48+
"pydantic-ai",
49+
]
50+
langchain = [
51+
# LangChain + LangGraph (kept optional to avoid forcing heavyweight deps on core SDK users)
52+
"langchain",
53+
"langgraph",
54+
]
4655
vision-local = [
4756
"pillow>=10.0.0",
4857
"torch>=2.2.0",

0 commit comments

Comments
 (0)