Skip to content

Commit b2780e1

Browse files
authored
Merge pull request #39 from WorkflowAI/guillaume/fix-replies
Fix replies and add use cache doc
2 parents b10118a + 19ad12a commit b2780e1

File tree

10 files changed

+178
-24
lines changed

10 files changed

+178
-24
lines changed

.github/workflows/examples.yml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Examples
2+
on:
3+
push:
4+
branches:
5+
- main
6+
schedule:
7+
# Every night at midnight
8+
- cron: "0 0 * * *"
9+
workflow_dispatch:
10+
11+
jobs:
12+
examples:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v3
16+
17+
- name: Install Poetry
18+
run: pipx install poetry==1.8.3
19+
20+
- name: Install dependencies
21+
run: poetry install --all-extras
22+
23+
- name: Run all example scripts
24+
run: |
25+
# Find all Python files in examples directory
26+
find examples -name "*.py" -type f | while read -r script; do
27+
echo "Running example: $script"
28+
poetry run python "$script"
29+
if [ $? -ne 0 ]; then
30+
echo "Error: Failed to run $script"
31+
exit 1
32+
fi
33+
done
34+
35+
- name: Send Slack Notification
36+
if: failure()
37+
env:
38+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
39+
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
40+
run: |
41+
curl -X POST -H 'Content-type: application/json' --data '{
42+
"blocks": [
43+
{
44+
"type": "section",
45+
"text": {
46+
"type": "mrkdwn",
47+
"text": ":warning: *Python Examples Failed!* \n <!channel>"
48+
}
49+
},
50+
{
51+
"type": "section",
52+
"fields": [
53+
{
54+
"type": "mrkdwn",
55+
"text": "*Job:* ${{ github.job }}"
56+
},
57+
{
58+
"type": "mrkdwn",
59+
"text": "*Run Number:* ${{ github.run_number }}"
60+
}
61+
]
62+
},
63+
{
64+
"type": "actions",
65+
"elements": [
66+
{
67+
"type": "button",
68+
"text": {
69+
"type": "plain_text",
70+
"text": "View Action Run"
71+
},
72+
"url": "${{ env.GITHUB_RUN_URL }}"
73+
}
74+
]
75+
}
76+
]
77+
}' $SLACK_WEBHOOK_URL

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,29 @@ async def analyze_call_feedback(input: CallFeedbackInput) -> AsyncIterator[Run[C
253253
...
254254
```
255255

256+
### Caching
257+
258+
By default, the cache settings is `auto`, meaning that agent runs are cached when the temperature is 0
259+
(the default temperature value). Which means that, when running the same agent twice with the **exact** same input,
260+
the exact same output is returned and the underlying model is not called a second time.
261+
262+
The cache usage string literal is defined in [cache_usage.py](./workflowai/core/domain/cache_usage.py) file. There are 3 possible values:
263+
264+
- `auto`: (default) Use cached results only when temperature is 0
265+
- `always`: Always use cached results if available, regardless of model temperature
266+
- `never`: Never use cached results, always execute a new run
267+
268+
The cache usage can be passed to the agent function as a keyword argument:
269+
270+
```python
271+
@workflowai.agent(id="analyze-call-feedback")
272+
async def analyze_call_feedback(_: CallFeedbackInput) -> AsyncIterator[CallFeedbackOutput]: ...
273+
274+
run = await analyze_call_feedback(CallFeedbackInput(...), use_cache="always")
275+
```
276+
277+
<!-- TODO: add cache usage at agent level when available -->
278+
256279
### Replying to a run
257280

258281
Some use cases require the ability to have a back and forth between the client and the LLM. For example:
@@ -275,7 +298,7 @@ async def say_hello(input: Input) -> Run[Output]:
275298
...
276299

277300
run = await say_hello(Input(name="John"))
278-
run = await run.reply(user_response="Now say hello to his brother James")
301+
run = await run.reply(user_message="Now say hello to his brother James")
279302
```
280303

281304
The output of a reply to a run has the same type as the original run, which makes it easy to iterate towards the

examples/__init__.py

Whitespace-only changes.

examples/reply/name_extractor.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
1+
import asyncio
2+
3+
from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType]
4+
15
import workflowai
26
from workflowai import Model, Run
3-
from pydantic import BaseModel, Field
4-
import asyncio
57

68

79
class NameExtractionInput(BaseModel):
810
"""Input containing a sentence with a person's name."""
11+
912
sentence: str = Field(description="A sentence containing a person's name.")
1013

1114

1215
class NameExtractionOutput(BaseModel):
1316
"""Output containing the extracted first and last name."""
17+
1418
first_name: str = Field(
1519
default="",
16-
description="The person's first name extracted from the sentence."
20+
description="The person's first name extracted from the sentence.",
1721
)
1822
last_name: str = Field(
1923
default="",
20-
description="The person's last name extracted from the sentence."
24+
description="The person's last name extracted from the sentence.",
2125
)
2226

2327

24-
@workflowai.agent(id='name-extractor', model=Model.GPT_4O_MINI_LATEST)
25-
async def extract_name(input: NameExtractionInput) -> Run[NameExtractionOutput]:
28+
@workflowai.agent(id="name-extractor", model=Model.GPT_4O_MINI_LATEST)
29+
async def extract_name(_: NameExtractionInput) -> Run[NameExtractionOutput]:
2630
"""
2731
Extract a person's first and last name from a sentence.
2832
Be precise and consider cultural variations in name formats.
@@ -38,21 +42,21 @@ async def main():
3842
"Dr. Maria Garcia-Rodriguez presented her research.",
3943
"The report was written by James van der Beek last week.",
4044
]
41-
45+
4246
for sentence in sentences:
4347
print(f"\nProcessing: {sentence}")
44-
48+
4549
# Initial extraction
4650
run = await extract_name(NameExtractionInput(sentence=sentence))
47-
51+
4852
print(f"Extracted: {run.output.first_name} {run.output.last_name}")
49-
53+
5054
# Double check with a simple confirmation
51-
run = await run.reply(user_response="Are you sure?")
52-
55+
run = await run.reply(user_message="Are you sure?")
56+
5357
print("\nAfter double-checking:")
5458
print(f"Final extraction: {run.output.first_name} {run.output.last_name}")
5559

5660

5761
if __name__ == "__main__":
58-
asyncio.run(main())
62+
asyncio.run(main())

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ unfixable = []
6565
"bin/*" = ["T201"]
6666
"*_test.py" = ["S101"]
6767
"conftest.py" = ["S101"]
68+
"examples/*" = ["INP001", "T201"]
6869

6970
[tool.pyright]
7071
pythonVersion = "3.9"

tests/e2e/reply_test.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from pydantic import BaseModel, Field # pyright: ignore [reportUnknownVariableType]
2+
3+
import workflowai
4+
from workflowai import Model, Run
5+
6+
7+
class NameExtractionInput(BaseModel):
8+
"""Input containing a sentence with a person's name."""
9+
10+
sentence: str = Field(description="A sentence containing a person's name.")
11+
12+
13+
class NameExtractionOutput(BaseModel):
14+
"""Output containing the extracted first and last name."""
15+
16+
first_name: str = Field(
17+
default="",
18+
description="The person's first name extracted from the sentence.",
19+
)
20+
last_name: str = Field(
21+
default="",
22+
description="The person's last name extracted from the sentence.",
23+
)
24+
25+
26+
@workflowai.agent(id="name-extractor", model=Model.GPT_4O_MINI_LATEST)
27+
async def extract_name(_: NameExtractionInput) -> Run[NameExtractionOutput]:
28+
"""
29+
Extract a person's first and last name from a sentence.
30+
Be precise and consider cultural variations in name formats.
31+
If multiple names are present, focus on the most prominent one.
32+
"""
33+
...
34+
35+
36+
async def test_reply():
37+
run = await extract_name(NameExtractionInput(sentence="My friend John Smith went to the store."))
38+
39+
assert run.output.first_name == "John"
40+
assert run.output.last_name == "Smith"
41+
42+
run = await run.reply(user_message="Are you sure?")
43+
44+
assert run.output.first_name == "John"
45+
assert run.output.last_name == "Smith"

workflowai/core/client/_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class RunRequest(BaseModel):
3535

3636

3737
class ReplyRequest(BaseModel):
38-
user_response: Optional[str] = None
38+
user_message: Optional[str] = None
3939
version: Union[str, int, dict[str, Any]]
4040
metadata: Optional[dict[str, Any]] = None
4141

workflowai/core/client/agent.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ async def _prepare_run(self, task_input: AgentInput, stream: bool, **kwargs: Unp
117117
async def _prepare_reply(
118118
self,
119119
run_id: str,
120-
user_response: Optional[str],
120+
user_message: Optional[str],
121121
tool_results: Optional[Iterable[ToolCallResult]],
122122
stream: bool,
123123
**kwargs: Unpack[RunParams[AgentOutput]],
@@ -127,7 +127,7 @@ async def _prepare_reply(
127127
version = self._sanitize_version(kwargs.get("version"))
128128

129129
request = ReplyRequest(
130-
user_response=user_response,
130+
user_message=user_message,
131131
version=version,
132132
stream=stream,
133133
metadata=kwargs.get("metadata"),
@@ -345,12 +345,12 @@ async def stream(
345345
async def reply(
346346
self,
347347
run_id: str,
348-
user_response: Optional[str] = None,
348+
user_message: Optional[str] = None,
349349
tool_results: Optional[Iterable[ToolCallResult]] = None,
350350
current_iteration: int = 0,
351351
**kwargs: Unpack[RunParams[AgentOutput]],
352352
):
353-
prepared_run = await self._prepare_reply(run_id, user_response, tool_results, stream=False, **kwargs)
353+
prepared_run = await self._prepare_reply(run_id, user_message, tool_results, stream=False, **kwargs)
354354
validator, new_kwargs = self._sanitize_validator(kwargs, intolerant_validator(self.output_cls))
355355

356356
res = await self.api.post(prepared_run.route, prepared_run.request, returns=RunResponse, run=True)

workflowai/core/domain/cache_usage.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
from typing import Literal
22

3-
CacheUsage = Literal["always", "never", "auto"]
3+
# Cache usage configuration for agent runs
4+
# - "auto": Use cached results only when temperature is 0
5+
# - "always": Always use cached results if available, regardless of model temperature
6+
# - "never": Never use cached results, always execute a new run
7+
CacheUsage = Literal["auto", "always", "never"]

workflowai/core/domain/run.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@ def __eq__(self, other: object) -> bool:
6767

6868
async def reply(
6969
self,
70-
user_response: Optional[str] = None,
70+
user_message: Optional[str] = None,
7171
tool_results: Optional[Iterable[ToolCallResult]] = None,
7272
**kwargs: Unpack["_common_types.RunParams[AgentOutput]"],
7373
):
7474
if not self._agent:
7575
raise ValueError("Agent is not set")
7676
return await self._agent.reply(
7777
run_id=self.id,
78-
user_response=user_response,
78+
user_message=user_message,
7979
tool_results=tool_results,
8080
**kwargs,
8181
)
@@ -91,9 +91,9 @@ class _AgentBase(Protocol, Generic[AgentOutput]):
9191
async def reply(
9292
self,
9393
run_id: str,
94-
user_response: Optional[str] = None,
94+
user_message: Optional[str] = None,
9595
tool_results: Optional[Iterable[ToolCallResult]] = None,
9696
**kwargs: Unpack["_types.RunParams[AgentOutput]"],
9797
) -> "Run[AgentOutput]":
98-
"""Reply to a run. Either a user_response or tool_results must be provided."""
98+
"""Reply to a run. Either a user_message or tool_results must be provided."""
9999
...

0 commit comments

Comments
 (0)