Files
langgraph/tests/test_pregel_async.py
T

321 lines
9.8 KiB
Python

import asyncio
import pytest
from langchain.schema.runnable import RunnablePassthrough
from pytest_mock import MockerFixture
from permchain import Pregel, channels
from permchain.pregel import PregelInvoke
async def test_invoke_single_process_in_out(mocker: MockerFixture) -> None:
add_one = mocker.Mock(side_effect=lambda x: x + 1)
chain = Pregel.subscribe_to("input") | add_one | Pregel.send_to("output")
app = Pregel(
(chain,),
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
},
input="input",
output="output",
)
assert await app.ainvoke(2) == 3
async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None:
add_one = mocker.Mock(side_effect=lambda x: x + 1)
chain_one = Pregel.subscribe_to("input") | add_one | Pregel.send_to("inbox")
chain_two = Pregel.subscribe_to_each("inbox") | add_one | Pregel.send_to("output")
app = Pregel(
[chain_one, chain_two],
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
"inbox": channels.Inbox(int),
},
input="input",
output="output",
)
assert await app.ainvoke(2) == 4
async def test_batch_two_processes_in_out() -> None:
async def add_one_with_delay(inp: int) -> int:
await asyncio.sleep(inp / 10)
return inp + 1
chain_one = (
Pregel.subscribe_to("input") | add_one_with_delay | Pregel.send_to("one")
)
chain_two = (
Pregel.subscribe_to("one") | add_one_with_delay | Pregel.send_to("output")
)
app = Pregel(
chain_one,
chain_two,
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
"one": channels.LastValue(int),
},
input="input",
output="output",
)
assert await app.abatch([3, 2, 1, 3, 5]) == [5, 4, 3, 5, 7]
async def test_invoke_many_processes_in_out(mocker: MockerFixture) -> None:
test_size = 100
add_one = mocker.Mock(side_effect=lambda x: x + 1)
chans = {
"input": channels.LastValue(int),
"output": channels.LastValue(int),
"-1": channels.LastValue(int),
}
chains: list[PregelInvoke] = [
Pregel.subscribe_to("input") | add_one | Pregel.send_to("-1")
]
for i in range(test_size - 2):
chans[str(i)] = channels.LastValue(int)
chains.append(
Pregel.subscribe_to(str(i - 1)) | add_one | Pregel.send_to(str(i))
)
chains.append(Pregel.subscribe_to(str(i)) | add_one | Pregel.send_to("output"))
app = Pregel(*chains, channels=chans, input="input", output="output")
# No state is left over from previous invocations
for _ in range(10):
assert await app.ainvoke(2, {"recursion_limit": test_size}) == 2 + test_size
# Concurrent invocations do not interfere with each other
assert await asyncio.gather(
*(app.ainvoke(2, {"recursion_limit": test_size}) for _ in range(10))
) == [2 + test_size for _ in range(10)]
async def test_batch_many_processes_in_out(mocker: MockerFixture) -> None:
test_size = 100
add_one = mocker.Mock(side_effect=lambda x: x + 1)
chans = {
"input": channels.LastValue(int),
"output": channels.LastValue(int),
"-1": channels.LastValue(int),
}
chains: list[PregelInvoke] = [
Pregel.subscribe_to("input") | add_one | Pregel.send_to("-1")
]
for i in range(test_size - 2):
chans[str(i)] = channels.LastValue(int)
chains.append(
Pregel.subscribe_to(str(i - 1)) | add_one | Pregel.send_to(str(i))
)
chains.append(Pregel.subscribe_to(str(i)) | add_one | Pregel.send_to("output"))
app = Pregel(*chains, channels=chans, input="input", output="output")
# No state is left over from previous invocations
for _ in range(10):
# Then invoke pubsub
assert await app.abatch([2, 1, 3, 4, 5], {"recursion_limit": test_size}) == [
2 + test_size,
1 + test_size,
3 + test_size,
4 + test_size,
5 + test_size,
]
# Concurrent invocations do not interfere with each other
assert await asyncio.gather(
*(
app.abatch([2, 1, 3, 4, 5], {"recursion_limit": test_size})
for _ in range(10)
)
) == [
[2 + test_size, 1 + test_size, 3 + test_size, 4 + test_size, 5 + test_size]
for _ in range(10)
]
async def test_invoke_two_processes_two_in_two_out_invalid(
mocker: MockerFixture,
) -> None:
add_one = mocker.Mock(side_effect=lambda x: x + 1)
chain_one = Pregel.subscribe_to("input") | add_one | Pregel.send_to("output")
chain_two = Pregel.subscribe_to("input") | add_one | Pregel.send_to("output")
app = Pregel(
chain_one,
chain_two,
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
},
input="input",
output="output",
)
with pytest.raises(channels.InvalidUpdateError):
# LastValue channels can only be updated once per iteration
await app.ainvoke(2)
async def test_invoke_two_processes_two_in_two_out_valid(mocker: MockerFixture) -> None:
add_one = mocker.Mock(side_effect=lambda x: x + 1)
chain_one = Pregel.subscribe_to("input") | add_one | Pregel.send_to("output")
chain_two = Pregel.subscribe_to("input") | add_one | Pregel.send_to("output")
app = Pregel(
chain_one,
chain_two,
channels={
"input": channels.LastValue(int),
"output": channels.Inbox(int),
},
input="input",
output="output",
)
# An Inbox channel accumulates updates into a sequence
assert await app.ainvoke(2) == (3, 3)
async def test_invoke_two_processes_two_in_join_two_out(mocker: MockerFixture) -> None:
add_one = mocker.Mock(side_effect=lambda x: x + 1)
add_10_each = mocker.Mock(side_effect=lambda x: sorted(y + 10 for y in x))
chain_one = Pregel.subscribe_to("input") | add_one | Pregel.send_to("inbox")
chain_three = Pregel.subscribe_to("input") | add_one | Pregel.send_to("inbox")
chain_four = Pregel.subscribe_to("inbox") | add_10_each | Pregel.send_to("output")
app = Pregel(
chain_one,
chain_three,
chain_four,
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
"inbox": channels.Inbox(int),
},
input="input",
output="output",
)
# Then invoke app
# We get a single array result as chain_four waits for all publishers to finish
# before operating on all elements published to topic_two as an array
for _ in range(100):
assert await app.ainvoke(2) == [13, 13]
assert await asyncio.gather(*(app.ainvoke(2) for _ in range(100))) == [
[13, 13] for _ in range(100)
]
async def test_invoke_join_then_call_other_pubsub(mocker: MockerFixture) -> None:
add_one = mocker.Mock(side_effect=lambda x: x + 1)
add_10_each = mocker.Mock(side_effect=lambda x: [y + 10 for y in x])
inner_app = Pregel(
Pregel.subscribe_to("input") | add_one | Pregel.send_to("output"),
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
},
input="input",
output="output",
)
chain_one = (
Pregel.subscribe_to("input") | add_10_each | Pregel.send_to("inbox_one").map()
)
chain_two = (
Pregel.subscribe_to("inbox_one")
| inner_app.map()
| sorted
| Pregel.send_to("outbox_one")
)
chain_three = Pregel.subscribe_to("outbox_one") | sum | Pregel.send_to("output")
app = Pregel(
chain_one,
chain_two,
chain_three,
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
"inbox_one": channels.Inbox(int),
"outbox_one": channels.LastValue(int),
},
input="input",
output="output",
)
# Then invoke pubsub
for _ in range(10):
assert await app.ainvoke([2, 3]) == 27
assert await asyncio.gather(*(app.ainvoke([2, 3]) for _ in range(10))) == [
27 for _ in range(10)
]
async def test_invoke_two_processes_one_in_two_out(mocker: MockerFixture) -> None:
add_one = mocker.Mock(side_effect=lambda x: x + 1)
chain_one = (
Pregel.subscribe_to("input")
| add_one
| Pregel.send_to(output=RunnablePassthrough(), between=RunnablePassthrough())
)
chain_two = Pregel.subscribe_to("between") | add_one | Pregel.send_to("output")
app = Pregel(
chain_one,
chain_two,
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
"between": channels.LastValue(int),
},
input="input",
output="output",
)
# Then invoke pubsub
assert [c async for c in app.astream(2)] == [3, 4]
async def test_invoke_two_processes_no_out(mocker: MockerFixture) -> None:
add_one = mocker.Mock(side_effect=lambda x: x + 1)
chain_one = Pregel.subscribe_to("input") | add_one | Pregel.send_to("between")
chain_two = Pregel.subscribe_to("between") | add_one
app = Pregel(
chain_one,
chain_two,
channels={
"input": channels.LastValue(int),
"output": channels.LastValue(int),
"between": channels.LastValue(int),
},
input="input",
output="output",
)
# Then invoke pubsub
# It finishes executing (once no more messages being published)
# but returns nothing, as nothing was published to OUT topic
assert await app.ainvoke(2) is None