ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
"""Case study: Banking / transactions.
|
||||
|
||||
Architectural pattern demonstrated: **cross-object balance invariant**.
|
||||
A transaction's validity depends on the sender account's current balance --
|
||||
the validator must read related state (the sender account) and compare to the
|
||||
proposed change. This pattern doesn't fit single-table CHECK constraints; it
|
||||
requires a runtime validator that reads cross-object state at write time.
|
||||
|
||||
Object types: account, transaction, audit_log
|
||||
Key invariants:
|
||||
- sender_account.balance >= amount before transaction commits
|
||||
- account ownership: only the account's owner can initiate transfers
|
||||
- transactions are immutable after creation (no UPDATE)
|
||||
- balance updates are reactions, not direct writes (single source of truth)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pedo.core.models import (
|
||||
AccessContext, DataObject, ObjectType, Operation,
|
||||
PermissionRule, PrivilegeType, ReactionDeclaration,
|
||||
Relationship, RelationshipAction,
|
||||
)
|
||||
from pedo.core.store import ObjectStore
|
||||
|
||||
|
||||
def validate_transaction_balance(proposed, existing, accessor, store):
|
||||
"""Sender account must have sufficient balance for the transfer."""
|
||||
if existing is not None:
|
||||
return "Transactions are immutable; cannot UPDATE"
|
||||
sender_id = proposed.content.get("sender_account_id")
|
||||
amount = proposed.content.get("amount", 0)
|
||||
if sender_id is None or amount is None:
|
||||
return "sender_account_id and amount are required"
|
||||
if amount <= 0:
|
||||
return f"Transfer amount must be positive, got {amount}"
|
||||
sender = store.raw_read(sender_id)
|
||||
if sender is None:
|
||||
return f"Sender account {sender_id} not found"
|
||||
if sender.type_name != "account":
|
||||
return f"sender_account_id must reference an account, not {sender.type_name}"
|
||||
if sender.content.get("balance", 0) < amount:
|
||||
return (f"Insufficient balance: account has "
|
||||
f"{sender.content.get('balance', 0)}, transfer requires {amount}")
|
||||
return True
|
||||
|
||||
|
||||
def validate_transaction_recipient_exists(proposed, existing, accessor, store):
|
||||
"""Recipient account must exist and accept transfers."""
|
||||
if existing is not None:
|
||||
return True
|
||||
recipient_id = proposed.content.get("recipient_account_id")
|
||||
if not recipient_id:
|
||||
return "recipient_account_id is required"
|
||||
recipient = store.raw_read(recipient_id)
|
||||
if recipient is None or recipient.type_name != "account":
|
||||
return f"Recipient account {recipient_id} not found"
|
||||
if recipient.content.get("status") == "frozen":
|
||||
return "Recipient account is frozen"
|
||||
return True
|
||||
|
||||
|
||||
def apply_transaction(event, store):
|
||||
"""Reaction: when a transaction commits, debit sender and credit recipient.
|
||||
The handler did not have to encode this -- the schema declares it."""
|
||||
sys_ctx = AccessContext(user_id="system", role="system", org_id=event["object_org"])
|
||||
sender_id = event["object_content"]["sender_account_id"]
|
||||
recipient_id = event["object_content"]["recipient_account_id"]
|
||||
amount = event["object_content"]["amount"]
|
||||
|
||||
sender = store.raw_read(sender_id)
|
||||
if sender is not None:
|
||||
store.update(sender_id,
|
||||
{"balance": sender.content.get("balance", 0) - amount},
|
||||
sys_ctx, _reaction_depth=event["depth"])
|
||||
recipient = store.raw_read(recipient_id)
|
||||
if recipient is not None:
|
||||
store.update(recipient_id,
|
||||
{"balance": recipient.content.get("balance", 0) + amount},
|
||||
sys_ctx, _reaction_depth=event["depth"])
|
||||
|
||||
|
||||
def register_banking_types(store: ObjectStore) -> None:
|
||||
store.register_reaction_handler("apply_transaction", apply_transaction)
|
||||
|
||||
# Account: owner-only management; balance is updated only by reactions.
|
||||
store.register_type(ObjectType(
|
||||
name="account",
|
||||
fields={"holder_name": "str", "balance": "int", "status": "str"},
|
||||
permission_rules=[
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "auditor"}),
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {"role": "admin"}),
|
||||
# Balance writes happen via the apply_transaction reaction (system role)
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "system"}),
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.WRITE, {"role": "admin"}),
|
||||
],
|
||||
default_policy=Operation.DENY,
|
||||
))
|
||||
|
||||
# Transaction: immutable after create; validators run cross-object checks.
|
||||
store.register_type(ObjectType(
|
||||
name="transaction",
|
||||
fields={"sender_account_id": "str", "recipient_account_id": "str",
|
||||
"amount": "int", "subject": "str"},
|
||||
permission_rules=[
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"is_owner": True}),
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "auditor"}),
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.READ, {"role": "system"}),
|
||||
PermissionRule(Operation.ACCEPT, PrivilegeType.INSERT, {}), # any authenticated user
|
||||
# No WRITE / UPDATE / DELETE rules: transactions are immutable.
|
||||
],
|
||||
validators=[validate_transaction_balance,
|
||||
validate_transaction_recipient_exists],
|
||||
reactions=[
|
||||
ReactionDeclaration(event="after_create", handler="apply_transaction"),
|
||||
],
|
||||
default_policy=Operation.DENY,
|
||||
))
|
||||
Reference in New Issue
Block a user