swisper-agent-sdk 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swisper_agent_sdk-0.1.0a1/.gitignore +8 -0
- swisper_agent_sdk-0.1.0a1/PKG-INFO +315 -0
- swisper_agent_sdk-0.1.0a1/PROMPTING.md +211 -0
- swisper_agent_sdk-0.1.0a1/README.md +285 -0
- swisper_agent_sdk-0.1.0a1/pyproject.toml +117 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/__init__.py +117 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/agent.py +278 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/auth.py +224 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/card_builder.py +657 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/contract.py +324 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/correlation.py +219 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/__init__.py +32 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/calendar_event.py +16 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/contact.py +15 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/document.py +16 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/email_draft.py +16 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/generic_data.py +18 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/location.py +16 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/search_result.py +15 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/smart_home_state.py +16 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_types/task_item.py +15 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/data_utils.py +658 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/delegation.py +228 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/domain_agent_interface.py +196 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/errors.py +25 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/events.py +277 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/examples/__init__.py +18 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/examples/bring_your_own_model_agent.py +86 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/examples/meal_planning_agent.py +202 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/examples/twelve_line_agent.py +31 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/harness.py +225 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/memory.py +102 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/metrics.py +75 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/nodes.py +172 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/prompt_policy.py +122 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/prompts/__init__.py +1 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/prompts/composer.py +122 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/prompts/scaffolding.py +247 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/result_contract/__init__.py +44 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/result_contract/adapter.py +96 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/result_contract/registry.py +68 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/result_contract/types.py +122 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/state_keys.py +38 -0
- swisper_agent_sdk-0.1.0a1/src/swisper_agent_sdk/state_types.py +109 -0
- swisper_agent_sdk-0.1.0a1/tests/test_declared_dependencies.py +146 -0
- swisper_agent_sdk-0.1.0a1/tests/test_install_smoke.py +122 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: swisper-agent-sdk
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: The published contract a partner builds a Swisper domain agent against
|
|
5
|
+
Project-URL: Homepage, https://github.com/fintama/helvetiq
|
|
6
|
+
Project-URL: Repository, https://github.com/fintama/helvetiq/tree/main/apps/backend/packages/swisper-agent-sdk
|
|
7
|
+
License: MIT
|
|
8
|
+
Keywords: a2a,agent,domain-agent,sdk,swisper
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: jsonpath-ng>=1.7
|
|
20
|
+
Requires-Dist: langchain-core>=0.1
|
|
21
|
+
Requires-Dist: langgraph>=0.2
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.2.2; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# swisper-agent-sdk
|
|
32
|
+
|
|
33
|
+
Build a Swisper domain agent.
|
|
34
|
+
|
|
35
|
+
This package is self-contained. You do not need the Swisper backend, a database,
|
|
36
|
+
or network access to build an agent and test it.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
Requires **Python 3.11 or newer**.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python3 -m venv .venv
|
|
44
|
+
./.venv/bin/pip install swisper-agent-sdk
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use the virtual environment's interpreter for everything below. Typing `python3`
|
|
48
|
+
rather than `./.venv/bin/python` is the most common way to get a
|
|
49
|
+
`ModuleNotFoundError` that looks like a broken package and is not.
|
|
50
|
+
|
|
51
|
+
> **Five declared dependencies pull in more than five packages.** This package
|
|
52
|
+
> declares `pydantic`, `langgraph`, `jsonpath-ng`, `pyyaml` and `langchain-core` —
|
|
53
|
+
> `langgraph` alone brings its own dependency tree with it (`langsmith`, `httpx`,
|
|
54
|
+
> `orjson`, `ormsgpack`, `zstandard` and more). Measured in a clean install: **36
|
|
55
|
+
> packages**, not five. None of that is a defect — every one of them is something
|
|
56
|
+
> `langgraph` genuinely needs — but a partner watching 36 packages scroll by after
|
|
57
|
+
> being told about five deserves to have been told first.
|
|
58
|
+
|
|
59
|
+
### From a directory, instead
|
|
60
|
+
|
|
61
|
+
Handed the package as a tarball or a source checkout rather than installing from
|
|
62
|
+
PyPI? Point `pip` at the directory instead of the package name — everything else
|
|
63
|
+
on this page works exactly the same either way:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python3 -m venv .venv
|
|
67
|
+
./.venv/bin/pip install -e ./swisper-agent-sdk
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Your first agent
|
|
71
|
+
|
|
72
|
+
This is `swisper_agent_sdk/examples/twelve_line_agent.py`, complete and
|
|
73
|
+
unabridged. It ships inside the package itself, so installing it is the only step
|
|
74
|
+
— there is nothing separate to fetch:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from swisper_agent_sdk import Agent, Delegation, Reply
|
|
78
|
+
|
|
79
|
+
agent = Agent(
|
|
80
|
+
name="echo_agent",
|
|
81
|
+
description="Repeats the task back. The smallest thing that is still an agent.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@agent.handler
|
|
86
|
+
async def handle(d: Delegation) -> Reply:
|
|
87
|
+
if not d.task:
|
|
88
|
+
return Reply.ask("What would you like me to do?")
|
|
89
|
+
return Reply(text=f"You asked me to: {d.task}")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Run it — the same command whether you installed from PyPI or from a directory:
|
|
93
|
+
|
|
94
|
+
```console
|
|
95
|
+
$ ./.venv/bin/python -m swisper_agent_sdk.examples.twelve_line_agent
|
|
96
|
+
You asked me to: book a train to Bern
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`d.task` is what the user asked for. Returning a `Reply` answers them;
|
|
100
|
+
returning `Reply.ask(...)` puts a question to them and pauses the turn.
|
|
101
|
+
|
|
102
|
+
## Running a turn yourself
|
|
103
|
+
|
|
104
|
+
`harness` runs a real turn against your agent with **no core, no database and
|
|
105
|
+
no network**:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import asyncio
|
|
109
|
+
from swisper_agent_sdk import harness
|
|
110
|
+
from my_agent import agent
|
|
111
|
+
|
|
112
|
+
result = asyncio.run(harness.turn(agent, "book a train to Bern"))
|
|
113
|
+
print(result.text)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Check your install
|
|
117
|
+
|
|
118
|
+
Needs the directory install above — `tests/` ships in a source checkout, not in
|
|
119
|
+
the plain PyPI wheel:
|
|
120
|
+
|
|
121
|
+
```console
|
|
122
|
+
$ cd swisper-agent-sdk
|
|
123
|
+
$ ../.venv/bin/pip install -e ".[dev]"
|
|
124
|
+
$ ../.venv/bin/python -m pytest
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
That runs a handful of smoke tests: the package imports, importing it leaves your
|
|
128
|
+
logging alone, an agent answers a turn offline, and every bundled example runs and
|
|
129
|
+
prints something. If those pass, the SDK works on your machine.
|
|
130
|
+
|
|
131
|
+
> **Run `pytest` from inside this directory.** The package's own
|
|
132
|
+
> `asyncio_mode = "auto"` setting lives in its `pyproject.toml`, and pytest only
|
|
133
|
+
> picks it up when invoked from here. Run it from the parent directory and every
|
|
134
|
+
> `async def` test fails — **including correct ones** — which looks like a broken
|
|
135
|
+
> SDK and is not.
|
|
136
|
+
|
|
137
|
+
`[dev]` is what installs `pytest` and `pytest-asyncio`. The base install
|
|
138
|
+
deliberately does not: it is the runtime, not the toolchain.
|
|
139
|
+
|
|
140
|
+
### Writing your own tests
|
|
141
|
+
|
|
142
|
+
`turn()` returns a `TurnResult` with `.text`, `.status`, `.resume_with`, `.cards`,
|
|
143
|
+
`.said`, and two helpers for assertions — `.said_something_about(...)` and
|
|
144
|
+
`.asked_for(...)`:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
async def test_it_answers():
|
|
148
|
+
r = await harness.turn(agent, "weather in Bern tomorrow")
|
|
149
|
+
assert r.said_something_about("Bern")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def test_it_can_ask_a_question():
|
|
153
|
+
r = await harness.turn(agent, "book me a train")
|
|
154
|
+
assert r.asked_for("departure time")
|
|
155
|
+
r = await harness.answer(r, "09:00") # resumes the paused turn
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`harness.answer(...)` resumes a turn your agent paused with `Reply.ask(...)`.
|
|
159
|
+
Inside the handler, `d.answer` is what the user replied and `d.resumed` is
|
|
160
|
+
`True`.
|
|
161
|
+
|
|
162
|
+
> **What a green harness run does and does not prove.** The harness fakes core,
|
|
163
|
+
> the capability endpoint and the token. A green harness run proves your agent's
|
|
164
|
+
> logic, not that the capability works against a live core. Those are different
|
|
165
|
+
> claims and only the first one is tested here.
|
|
166
|
+
|
|
167
|
+
## Saying things, and sending cards
|
|
168
|
+
|
|
169
|
+
Two ways to put something in front of the user besides your final answer.
|
|
170
|
+
|
|
171
|
+
**`await d.say("…")`** narrates a deterministic moment — typically just before a slow
|
|
172
|
+
call, where waiting for the model to decide to mention it is the wrong behaviour:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
await d.say("Checking which trains actually run at that hour…")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**`await d.card("swi-transport", payload)`** emits a card. **The types your agent may
|
|
179
|
+
emit are declared on the agent**, and an undeclared type raises
|
|
180
|
+
`UndeclaredCardTypeError` — never a silent drop:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
agent = Agent(name="…", description="…", cards=["swi-transport"])
|
|
184
|
+
|
|
185
|
+
await d.card("swi-transport", {"from": "Zurich", "to": "Bern"}) # fine
|
|
186
|
+
await d.card("swi-invented", {...}) # UndeclaredCardTypeError
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The declared list is also what the prompt tells the model about, so it cannot invent a
|
|
190
|
+
card type that has no renderer. Both are visible in tests as `result.said` and
|
|
191
|
+
`result.cards`.
|
|
192
|
+
|
|
193
|
+
## A fuller example
|
|
194
|
+
|
|
195
|
+
`swisper_agent_sdk/examples/meal_planning_agent.py` is the one to read once the
|
|
196
|
+
twelve-line agent makes sense. It is a working agent that recalls what it knows
|
|
197
|
+
about you, asks a question when it does not know enough, resumes on your answer,
|
|
198
|
+
and emits a card:
|
|
199
|
+
|
|
200
|
+
```console
|
|
201
|
+
$ ./.venv/bin/python -m swisper_agent_sdk.examples.meal_planning_agent
|
|
202
|
+
[turn 1] status=waiting_for_input text="I don't have any dietary restrictions or allergies on file for you — could you tell me what I should plan around?"
|
|
203
|
+
[turn 2] status=complete text="Here's a plan built around: I'm vegetarian and allergic to peanuts.."
|
|
204
|
+
cards emitted: ['meal_plan']
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
It also shows how to reach data you are entitled to. Declare what you need on the
|
|
208
|
+
agent and read it from the delegation:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
agent = Agent(..., inputs=("fact_lookup_service",))
|
|
212
|
+
|
|
213
|
+
# in the handler:
|
|
214
|
+
facts = await d.fact_lookup_service.get_by_type("ALLERGY")
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**You only receive what you declare.** Anything you did not ask for is absent from
|
|
218
|
+
your delegation, not present-and-empty.
|
|
219
|
+
|
|
220
|
+
> 🔴 **`fact_lookup_service` works in-process and is withheld over the wire.** It is a
|
|
221
|
+
> live service object, so it cannot be serialised to a remotely deployed agent — core
|
|
222
|
+
> drops it from the envelope unconditionally. **The example above runs green under the
|
|
223
|
+
> harness and returns nothing once your agent is deployed remotely, with no error.**
|
|
224
|
+
>
|
|
225
|
+
> This is stated here because it is the one place where following our own documentation
|
|
226
|
+
> produces a silent failure. Facts are meant to reach a remote agent **pre-loaded onto
|
|
227
|
+
> the envelope**, declared in the agent's contract — **that path is seamed but not yet
|
|
228
|
+
> wired, so a remote agent currently receives no pre-loaded facts.** If you are building
|
|
229
|
+
> for remote deployment, do not design around `fact_lookup_service` yet; talk to us.
|
|
230
|
+
|
|
231
|
+
## Two ways to write an agent, one runtime
|
|
232
|
+
|
|
233
|
+
`@agent.handler` is not a simplified mode you outgrow. **A handler is compiled
|
|
234
|
+
into a one-node graph**, so it runs through exactly the same machinery as an
|
|
235
|
+
agent you author as a graph yourself:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
agent = Agent(name="…", description="…", graph=my_compiled_graph)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Start with a handler. Move to `graph=` when you need more than one node. You are
|
|
242
|
+
not choosing a path you have to undo later.
|
|
243
|
+
|
|
244
|
+
## Telling the model how to behave
|
|
245
|
+
|
|
246
|
+
> 🔴 **No planner ships in this package.** `instructions=`, `routing=`,
|
|
247
|
+
> `narration_style=`, `tool_guidance=`, `prompt=` and `tools=` are stored on the
|
|
248
|
+
> `Agent` and are **not read when your agent runs**. Nothing here calls a model.
|
|
249
|
+
>
|
|
250
|
+
> To use a composed prompt today, call **`agent.compose_prompt(...)` yourself** and
|
|
251
|
+
> drive your own model with it — see
|
|
252
|
+
> [`examples/bring_your_own_model_agent.py`](src/swisper_agent_sdk/examples/bring_your_own_model_agent.py).
|
|
253
|
+
> A handler-mode agent decides everything in Python.
|
|
254
|
+
>
|
|
255
|
+
> **Everything in this section describes what `compose_prompt()` returns, not what
|
|
256
|
+
> the runtime does with it.**
|
|
257
|
+
|
|
258
|
+
Most agents need one thing — `instructions=`:
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
agent = Agent(
|
|
262
|
+
name="weather_agent",
|
|
263
|
+
description="Answers weather and forecast questions.",
|
|
264
|
+
instructions="""
|
|
265
|
+
You answer questions about weather and forecasts.
|
|
266
|
+
Prefer official meteorological sources over aggregators.
|
|
267
|
+
Never speculate beyond seven days — say you don't know instead.
|
|
268
|
+
""",
|
|
269
|
+
)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Your `instructions` reach the model verbatim.** We never rewrite them, and they
|
|
273
|
+
are not a template: braces inside them are literal text, so you cannot
|
|
274
|
+
accidentally interpolate one of our internal variables.
|
|
275
|
+
|
|
276
|
+
The SDK composes the rest of the planner prompt around them — the planning loop,
|
|
277
|
+
how to ask a question and resume, how to narrate, and a context block carrying
|
|
278
|
+
the user's language, locale, timezone, today's date and their temporal context.
|
|
279
|
+
|
|
280
|
+
**That context block is why this wrapper exists.** Omit the language instruction
|
|
281
|
+
and you answer a German user in English. Omit the locale rules and dates come out
|
|
282
|
+
American. Omit temporal context and "tomorrow" is the wrong day. None of these
|
|
283
|
+
fail a test you would think to write, and all of them reach a user.
|
|
284
|
+
|
|
285
|
+
If you want to replace one composed section rather than all of it, pass
|
|
286
|
+
`narration_style=`, `tool_guidance=` or `routing=`. If you want full control,
|
|
287
|
+
pass `prompt=` — and `swisper_agent_sdk.prompts.scaffolding()` is importable, so
|
|
288
|
+
you can still assemble the parts you did not want to write yourself.
|
|
289
|
+
|
|
290
|
+
📖 **[`PROMPTING.md`](PROMPTING.md)** has the full map of what gets composed, plus
|
|
291
|
+
two rules that are not general prompting advice — they are things we got wrong in
|
|
292
|
+
production, in ways that reached users. Read it before you write your second
|
|
293
|
+
agent.
|
|
294
|
+
|
|
295
|
+
## What this package deliberately does not give you
|
|
296
|
+
|
|
297
|
+
Said up front, because discovering it by hitting a wall is worse:
|
|
298
|
+
|
|
299
|
+
- **No `swisper.*` imports.** The SDK never reaches into the Swisper backend, and
|
|
300
|
+
a build check enforces that on both `src/` and `examples/`. If something you
|
|
301
|
+
need is missing here, that is our gap to close — tell us rather than working
|
|
302
|
+
around it.
|
|
303
|
+
- **No database access.** Your agent never opens a database connection. Data you
|
|
304
|
+
are entitled to arrives through a capability call that core authorises for that
|
|
305
|
+
turn.
|
|
306
|
+
- **Five declared dependencies**, deliberately: `pydantic`, `langgraph`,
|
|
307
|
+
`jsonpath-ng`, `pyyaml` and `langchain-core`. A test asserts that every import
|
|
308
|
+
is declared and every declaration is imported — not a count — so a new one
|
|
309
|
+
cannot arrive as a side effect of a convenient import, undeclared. (See the
|
|
310
|
+
install-time note above: `langgraph` alone brings a much larger dependency
|
|
311
|
+
tree with it — the count above is what *we* declare, not what actually
|
|
312
|
+
installs.)
|
|
313
|
+
- **The SDK does not configure logging.** Importing it leaves your root logger
|
|
314
|
+
untouched. Call `swisper_agent_sdk.correlation.configure_logging()` yourself if
|
|
315
|
+
you want ours.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Prompting a Swisper agent
|
|
2
|
+
|
|
3
|
+
> 🔴 **Read this first: no planner ships in this package.**
|
|
4
|
+
>
|
|
5
|
+
> `instructions=`, `routing=`, `narration_style=`, `tool_guidance=`, `prompt=` and
|
|
6
|
+
> `tools=` are stored on the `Agent` and are **not read when your agent runs**.
|
|
7
|
+
> Nothing in this package calls a model.
|
|
8
|
+
>
|
|
9
|
+
> **`compose_prompt()` genuinely works** — it returns the assembled prompt with your
|
|
10
|
+
> `instructions` verbatim inside it. But *you* call it, *you* supply the model, and
|
|
11
|
+
> *you* drive the loop. See
|
|
12
|
+
> [`examples/bring_your_own_model_agent.py`](src/swisper_agent_sdk/examples/bring_your_own_model_agent.py).
|
|
13
|
+
>
|
|
14
|
+
> **So read every present-tense sentence below as describing what
|
|
15
|
+
> `compose_prompt()` returns**, not what the runtime does with it. The rules are
|
|
16
|
+
> real and worth following — they are what we learned building agents on this
|
|
17
|
+
> prompt — but the composition is yours to invoke.
|
|
18
|
+
|
|
19
|
+
Two rules we paid to learn, a map of the prompt the SDK builds for you, and one
|
|
20
|
+
thing your tests cannot tell you.
|
|
21
|
+
|
|
22
|
+
These are not general prompting advice — there is plenty of that elsewhere. Each
|
|
23
|
+
rule here is something we got wrong first, in production, in a way that reached
|
|
24
|
+
users.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Rule 1 — "when to call this tool" belongs in the tool, not the prompt
|
|
29
|
+
|
|
30
|
+
Put the criteria for calling a tool **in that tool's own description**. Keep the
|
|
31
|
+
planner prompt for **cross-tool sequencing only** — what has to happen before
|
|
32
|
+
what.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# Good: the criterion travels with the tool.
|
|
36
|
+
def get_forecast(city: str, days: int = 3) -> dict:
|
|
37
|
+
"""Return the weather forecast for a city.
|
|
38
|
+
|
|
39
|
+
Call this when the user asks about future conditions. For what the weather
|
|
40
|
+
is doing right now, call get_current instead — this one is modelled and
|
|
41
|
+
coarse for today.
|
|
42
|
+
"""
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
# Bad: the same criterion in your `instructions=`.
|
|
47
|
+
Use get_forecast when the user asks about future weather, and get_current
|
|
48
|
+
when they ask about now.
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Why.** The second version is correct on the day you write it and rots on its
|
|
52
|
+
own. When someone changes `get_forecast`, they change the function and its
|
|
53
|
+
docstring — they do not go looking through a prompt for a sentence about it.
|
|
54
|
+
The rule and the thing it describes drift apart silently, and the prompt wins,
|
|
55
|
+
because that is what the model reads.
|
|
56
|
+
|
|
57
|
+
Written in the description, the criterion is changed by the same edit that
|
|
58
|
+
changes the behaviour. That is the whole argument: **put the rule where the next
|
|
59
|
+
person editing the code will already be standing.**
|
|
60
|
+
|
|
61
|
+
The SDK derives your `## Your tools` block from the registered functions'
|
|
62
|
+
signatures and full docstrings, so anything you write there reaches the model.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Rule 2 — write a concept and a test question, never a keyword list
|
|
67
|
+
|
|
68
|
+
When you need the model to classify something, describe the **idea** and give it
|
|
69
|
+
a question it can ask itself. Do not enumerate the words.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
# Good: a concept plus a test the model can apply.
|
|
73
|
+
Some facts are sensitive. The test: would the person be embarrassed if this
|
|
74
|
+
appeared on a shared screen without warning? Health, finances, relationships
|
|
75
|
+
and legal matters usually pass that test; a favourite coffee order does not.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
# Bad: a keyword table.
|
|
80
|
+
Treat as sensitive if the message contains: diagnosis, illness, therapy,
|
|
81
|
+
salary, debt, divorce, lawyer, ...
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Why.** Keyword lists fail on the two things that are guaranteed to happen.
|
|
85
|
+
|
|
86
|
+
**They fail on other languages.** Swisper answers in the user's language. A list
|
|
87
|
+
written in English does not fire on *Diagnose*, *Schulden* or *scheiden* — and
|
|
88
|
+
the failure is silent, because a classifier that never fires looks exactly like a
|
|
89
|
+
classifier with nothing to find.
|
|
90
|
+
|
|
91
|
+
**They fail on typos.** `divorce` and `divorec` are the same disclosure to a
|
|
92
|
+
human and different strings to a matcher. Real messages contain typos at a rate
|
|
93
|
+
that makes this routine, not an edge case.
|
|
94
|
+
|
|
95
|
+
A concept generalises to both for free. Keep examples — they are useful — but
|
|
96
|
+
write them as *illustrations of the idea*, never as the set to be matched.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## What the SDK composes around your `instructions`
|
|
101
|
+
|
|
102
|
+
At level 1 you write `instructions=` and the SDK assembles this:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
[SYSTEM]
|
|
106
|
+
├─ planner scaffolding the iterative loop, batching
|
|
107
|
+
├─ HITL rules how to ask, how to resume, how to escape
|
|
108
|
+
├─ card instructions when to emit; only the types you declared
|
|
109
|
+
├─ memory guidance when recall is worth calling
|
|
110
|
+
│
|
|
111
|
+
├─ ## Your domain ← your `instructions`, VERBATIM
|
|
112
|
+
├─ ## Your tools ← derived from your registered functions
|
|
113
|
+
├─ ## Context ← language · locale · timezone · date · temporal
|
|
114
|
+
│
|
|
115
|
+
└─ narration instructions ALWAYS LAST — see below
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Your `instructions` are interpolated verbatim and never rewritten.** They are
|
|
119
|
+
not a template: braces inside them are literal, so `{{ user_id }}` in your text
|
|
120
|
+
reaches the model as those characters and cannot pull in one of our variables.
|
|
121
|
+
|
|
122
|
+
**At level 2** you replace one named block and leave the rest composed:
|
|
123
|
+
`narration_style=` replaces the narration instructions, `tool_guidance=` replaces
|
|
124
|
+
the content under `## Your tools`, `routing=` replaces the HITL rules.
|
|
125
|
+
|
|
126
|
+
**At level 3** you pass `prompt=` and own the whole thing. If you want some of
|
|
127
|
+
the scaffolding back, `swisper_agent_sdk.prompts.scaffolding()` is importable and
|
|
128
|
+
returns the blocks individually.
|
|
129
|
+
|
|
130
|
+
### Narration is appended last, and that is deliberate
|
|
131
|
+
|
|
132
|
+
The narration block is always the final thing in the assembled prompt, at every
|
|
133
|
+
level. **Placement is load-bearing.** A tone instruction sitting thousands of
|
|
134
|
+
characters before the end does not land; the same words at the end do. If you
|
|
135
|
+
override it with `narration_style=`, your text takes that position.
|
|
136
|
+
|
|
137
|
+
⚠️ **The default narration examples are travel-flavoured** — trains, stations,
|
|
138
|
+
fares. They are there as *mechanism*: positive examples shape the model's output
|
|
139
|
+
far more reliably than instructions about what not to do. If you are building a
|
|
140
|
+
weather or finance agent, the shape still transfers, but consider passing
|
|
141
|
+
`narration_style=` with three examples from your own domain. Keep three, and keep
|
|
142
|
+
them concrete.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## What a green test run does not tell you
|
|
147
|
+
|
|
148
|
+
> **The harness fakes core, the capability endpoint and the token. A green
|
|
149
|
+
> harness run proves your agent's logic, not that the capability works against a
|
|
150
|
+
> live core.**
|
|
151
|
+
|
|
152
|
+
Those are different claims and only the first one is tested offline.
|
|
153
|
+
|
|
154
|
+
This is not a defect in the harness — faking the capability endpoint is exactly
|
|
155
|
+
what lets your tests run with no core, no database and no network, which is worth
|
|
156
|
+
far more than the certainty you give up. But it means a passing suite is evidence
|
|
157
|
+
about **your** code, not about the integration.
|
|
158
|
+
|
|
159
|
+
The failure it protects you from: your tests are green, you run against a real
|
|
160
|
+
core, `recall` returns nothing, and you spend an afternoon debugging your own
|
|
161
|
+
agent. It is not your agent. Check the integration separately, and treat the
|
|
162
|
+
first live run as its own test.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## `inputs=` is two mechanisms wearing one name
|
|
167
|
+
|
|
168
|
+
You declare what your agent receives with `inputs=(...)`. **What that declaration does
|
|
169
|
+
depends on where your agent runs, and the two behaviours are not the same:**
|
|
170
|
+
|
|
171
|
+
| | in-process | deployed remotely |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `inputs=` | an **enforced allowlist** — you get what you declared, and nothing else | **no such concept exists on the wire path** |
|
|
174
|
+
| `fact_lookup_service` | reaches your handler | **withheld unconditionally** — it is a live object and cannot serialise |
|
|
175
|
+
|
|
176
|
+
**The consequence is the one worth remembering: a handler that reads
|
|
177
|
+
`d.fact_lookup_service` passes every offline test and returns nothing in production.**
|
|
178
|
+
The harness runs the in-process path, so it cannot show you this. Nothing raises.
|
|
179
|
+
|
|
180
|
+
**And the contract file cannot tell the two apart.** `agent.contract.yaml` describes an
|
|
181
|
+
agent without knowing where it will run, so a declaration that is enforcement in one
|
|
182
|
+
deployment is decoration in the other.
|
|
183
|
+
|
|
184
|
+
Facts are meant to reach a remote agent **pre-loaded onto the envelope**, with the types
|
|
185
|
+
declared in its contract rather than fetched live. **That seam exists and is not yet
|
|
186
|
+
wired: no agent declares a pre-loaded type today, so a remote agent receives none.**
|
|
187
|
+
Stated in the present tense on purpose — it is a real limitation now, not a promise.
|
|
188
|
+
|
|
189
|
+
**The general lesson, which outlives this particular field:** when the same name means
|
|
190
|
+
different things on two paths, the one your tests exercise is the one you will believe.
|
|
191
|
+
Ask which path a green run actually walked.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Context you get for free, and why it matters
|
|
196
|
+
|
|
197
|
+
The `## Context` block carries the user's language, locale, timezone, today's
|
|
198
|
+
date and their temporal context. You do not have to ask for any of it.
|
|
199
|
+
|
|
200
|
+
This exists because every one of these has shipped as a real defect:
|
|
201
|
+
|
|
202
|
+
| omitted | what the user got |
|
|
203
|
+
|---|---|
|
|
204
|
+
| language instruction | a German user answered in English |
|
|
205
|
+
| locale formatting | `03/04` meaning April 3rd to a Swiss reader |
|
|
206
|
+
| temporal context | "tomorrow" resolved to the wrong day |
|
|
207
|
+
|
|
208
|
+
**None of them fail a test you would think to write.** All of them reach a user,
|
|
209
|
+
and all of them get reported to us as Swisper bugs. That is why this block is
|
|
210
|
+
composed rather than left to you — not because you would get it wrong, but
|
|
211
|
+
because you would have to know it was there to get it right.
|