Резултат агента може имати исправан JSON облик, а погрешно прочитати извор. Овај водич даје Python програмеру потпун пут од једне дозвољене датотеке бележака до локалног регистра: типизован резултат произвођача, провере извора и значења, људско одобрење тачних хешева и потрошач који уписује и поново чита груписан Markdown извештај. Користите своје белешке са изричитим обавезама. За припрему нису коришћене стварне белешке ни резултат модела.

Велика промена

  • Шта се променило: OpenAI Agents SDK прихвата Pydantic output_type, па апликација може тражити дефинисан резултат и проверити га пре употребе. Пример додаје изворне цитате и верзионисан уговор око резултата.
  • Зашто је важно: Програмер који повезује агента са следећим кораком може зауставити неправилан, непотпун или непоткрепљен излаз пре него што постане списак радњи. Рецензент и даље мора проценити да ли цитиране белешке стварно подржавају сваки задатак, одговорно лице и датум и да ли је модел нешто пропустио.
  • На шта пазити: Одлука се доноси на граници потрошача. Поступак прави само локални регистар за тријажу након одобрења тачног извршавања, извора и кандидата. Каснији увоз у систем задатака тражи своја овлашћења, правила понављања и проверу читањем на одредишту.

Припремите један одобрен извор

Потребни су Python 3.12, OpenAI API налог одобрен за белешке, рецензент који може читати оригинал и модел чију сте тренутну подршку за JSON-schema структурисан излаз потврдили на налогу. Проверите услове података и актуелне цене пре покретања произвођача. Не стављајте приступне податке у белешке или одобрење. Једини позив моделу испод је produce; остало је локална обрада.

Сачувајте оригиналне белешке као UTF-8 у notes.txt. Морају садржати стварне изричите обавезе из вашег рада. Доделите стабилан source_id, на пример свој идентификатор записа, и не мењајте ниједан бајт до прегледа и обраде. Направите candidate/ пре производње; датотека кандидата мора бити нова. Обезбедите одвојен излазни директоријум за каснији локални извештај.

Пре извршавања забележите хеш бајтова извора:

Terminal
python -c "from pathlib import Path; import hashlib; print(hashlib.sha256(Path('notes.txt').read_bytes()).hexdigest())"

У изолованом Python 3.12 окружењу направите recipe-requirements.txt са овим тачним редовима и инсталирајте:

Text
openai-agents==0.24.0
pydantic==2.14.1
Terminal
python -m pip install -r recipe-requirements.txt
mkdir candidate

Ове верзије су заједно инсталиране у локалном Python 3.12.7 окружењу 20. октобра 2026. Потврдите компатибилност и доступност модела при коришћењу. Сачувајте комплетан скрипт испод као action_register_recipe.py у истом директоријуму. Има две команде: produce прави кандидата и потврду извршавања; consume прави извештај тек после валидације и одобрења.

Покрените типизованог произвођача

Уговор Extraction захтева верзију action-register/v1, статус ready, partial или blocked, ID извора и SHA-256 тачних бајтова, радње и gap_note. Свака радња захтева текст, тачан цитат и распон изворних редова бројан од 1, одговорно лице и датум који могу бити null, као и complete или needs_review као статус доказа. Null одговорно лице или датум значи да нису наведени у белешкама. Модел добија налог да их не закључује. Сва поља су обавезна и додатна се одбијају.

Изричито изаберите модел за структурисан излаз. Дајте своју дозвољену путању и идентификатор:

Terminal
python action_register_recipe.py produce --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --model YOUR-STRUCTURED-OUTPUT-MODEL

Прочитајте candidate/actions.json поред candidate/actions.run.json. Друга датотека бележи UUID извршавања, хешеве извора и кандидата, завршног агента и тражени модел. Грешка уместо тога прави дијагностички запис .failure.json са ID-јем извршавања у суфиксу. Прекид, None или завршни излаз као обичан текст, или неочекиван завршни агент, никада нису успех. Команда не чува настављиво SDK стање RunState; потврда грешке не може наставити извршавање. Прегледајте захтев, кандидата и низводно стање пре одлуке о новом покретању.

Прегледајте значење и одобрите тачног кандидата

Апликација проверава строге типове и обавезна поља, идентитет извора, спреман статус, бар једну потпуну радњу, важеће распоне, присуство цитата у наведеном распону, непразан задатак или власника и канонске датуме YYYY-MM-DD. То доказује структуру и место цитата. Не утврђује да је реченица стварна обавеза, да име или датум значе оно што кандидат тврди или да су пронађене све обавезе.

Прочитајте оригиналне белешке и сваку предложену радњу. Поредите значење, одговорно лице, датум, цитат и пропусте. Нејасноћу извора или кандидата решите новим прегледаним извршавањем; непоткрепљене, делимичне или блокиране кандидате оставите без одобрења. Када проверите да непромењена датотека и кандидат одговарају actions.run.json, напишите approval.json са тачно ових седам кључева. Копирајте ID и хешеве из проверене потврде. Користите стварни идентификатор рецензента и садржајан опис стварно обављених провера:

JSON
{
  "approved": true,
  "run_id": "COPY-RUN-ID",
  "source_id": "OWNED-ID",
  "source_sha256": "COPY-SOURCE-SHA256",
  "payload_sha256": "COPY-PAYLOAD-SHA256",
  "reviewer": "YOUR-REVIEWER-ID",
  "review_note": "Describe the source, task, owner, date, quotation and omission checks actually performed."
}

approved мора бити JSON логичка вредност true, не текст или број. Све друге вредности морају бити стрингови; рецензент и напомена не смеју бити празни. Додатна поља падају. Одобрење везује једног кандидата и ревизију извора, не будућа преписивања.

Обрадите и поново прочитајте извештај

Terminal
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/actions.json --approval approval.json --out private-output

Потрошач групише прихваћене радње у Assigned with dates, Needs an owner и Needs a date. Свака ставка садржи задатак, стање одговорног лица и датума, цитат и изворне редове. Уписује action-register-<payload-hash-prefix>.md и суседну датотеку .receipt.json са хешевима извршавања, извора, кандидата и извештаја. Отворите обе, упоредите цитиране радње са белешкама и проверите report_sha256 према бајтовима Markdown датотеке. Важећа потврда каже само да је локални резултат направљен и поново прочитан; не одобрава упис у спољни систем задатака.

Команда обраде исписује стварне путање извештаја и потврде. Користите их за проверу бајтова:

Terminal
python -c "from pathlib import Path; import hashlib,json,sys; report=Path(sys.argv[1]); receipt=json.loads(Path(sys.argv[2]).read_text(encoding='utf-8')); assert hashlib.sha256(report.read_bytes()).hexdigest()==receipt['report_sha256']; print('report hash matches receipt')" PATH-PRINTED-FOR-REPORT PATH-PRINTED-FOR-RECEIPT

Када имате кандидата, проверите пут одбијања јасно синтетичким структурним примером. Копирајте JSON у одвојену датотеку, из прве радње уклоните било које обавезно поље и позовите consume са неисправном датотеком и новим празним излазним директоријумом. Мора пасти пре стварања резултата. Пример никада не третирајте као чињенично издвајање нити га одобравајте. Следећа локална провера, на пример, из копије уклања evidence_quote:

Terminal
python -c "import json; from pathlib import Path; p=json.loads(Path('candidate/actions.json').read_text(encoding='utf-8')); p['actions'][0].pop('evidence_quote'); Path('candidate/SYNTHETIC-malformed.json').write_text(json.dumps(p), encoding='utf-8')"
python action_register_recipe.py consume --source-id OWNED-ID --source notes.txt --candidate candidate/SYNTHETIC-malformed.json --approval approval.json --out synthetic-rejection-output

Друга команда треба да падне, а излазни директоријум да остане непостојећи. Не користите пример ако прави кандидат нема прву радњу; већ је непотпун за овог потрошача. Код неизвесног уписа прегледајте детерминистички извештај и потврду пре понављања. Исти постојећи бајтови могу се поново користити; различити заустављају ради усаглашавања. Промењен извор или верзија шеме траже нови преглед. Не понављајте позив моделу само зато што је одговор локалног потрошача био нејасан.

Цео Python поступак

Копирајте овај скрипт тачно у action_register_recipe.py:

Python
"""Private, bounded recipe. No model call occurs unless `produce` is explicitly run."""

import argparse
import hashlib
import json
import uuid
from datetime import date
from pathlib import Path
from typing import Literal

from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr, ValidationError


class StrictModel(BaseModel):
    model_config = ConfigDict(extra="forbid", strict=True)


class Action(StrictModel):
    action_text: StrictStr
    evidence_quote: StrictStr
    line_start: StrictInt
    line_end: StrictInt
    owner: StrictStr | None
    due_date: StrictStr | None
    evidence_status: Literal["complete", "needs_review"]


class Extraction(StrictModel):
    schema_version: Literal["action-register/v1"]
    status: Literal["ready", "partial", "blocked"]
    source_id: StrictStr
    source_sha256: StrictStr
    actions: list[Action]
    gap_note: StrictStr | None


def digest(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()


def canonical(payload: Extraction) -> bytes:
    return json.dumps(payload.model_dump(mode="json"), sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8")


def check(payload: Extraction, source_id: str, source: bytes) -> None:
    if payload.source_id != source_id or payload.source_sha256 != digest(source):
        raise ValueError("source identity or revision mismatch")
    lines = source.decode("utf-8").splitlines()
    if payload.status == "ready" and (not payload.actions or payload.gap_note is not None):
        raise ValueError("ready requires actions and no gap")
    if payload.status != "ready":
        raise ValueError("partial or blocked result cannot reach consumer")
    for index, item in enumerate(payload.actions, 1):
        if item.evidence_status != "complete":
            raise ValueError(f"action {index}: evidence needs review")
        if not item.action_text.strip() or not item.evidence_quote.strip():
            raise ValueError(f"action {index}: empty task or evidence")
        if not 1 <= item.line_start <= item.line_end <= len(lines):
            raise ValueError(f"action {index}: invalid source line range")
        segment = "\n".join(lines[item.line_start - 1:item.line_end])
        if item.evidence_quote not in segment:
            raise ValueError(f"action {index}: quotation absent from source span")
        if item.owner is not None and not item.owner.strip():
            raise ValueError(f"action {index}: blank owner")
        if item.due_date is not None:
            try:
                if date.fromisoformat(item.due_date).isoformat() != item.due_date:
                    raise ValueError()
            except ValueError as exc:
                raise ValueError(f"action {index}: due date must be YYYY-MM-DD") from exc


def report(payload: Extraction) -> str:
    """Application consumer: a triage register grouped by missing ownership/date."""
    groups = [
        ("Assigned with dates", lambda x: x.owner is not None and x.due_date is not None),
        ("Needs an owner", lambda x: x.owner is None),
        ("Needs a date", lambda x: x.owner is not None and x.due_date is None),
    ]
    rows = ["# Action register for " + payload.source_id, "", "Review artifact; source meaning was approved separately.", ""]
    for heading, predicate in groups:
        rows += ["## " + heading, ""]
        matches = [(n, a) for n, a in enumerate(payload.actions, 1) if predicate(a)]
        for number, action in matches:
            rows += [f"{number}. {action.action_text}", f"   Owner: {action.owner or 'Unspecified'}; due: {action.due_date or 'Unspecified'}", f"   Source lines {action.line_start}-{action.line_end}: {action.evidence_quote}", ""]
        if not matches:
            rows += ["None.", ""]
    return "\n".join(rows)


def load_payload(path: Path) -> Extraction:
    return Extraction.model_validate_json(path.read_bytes(), strict=True)


def consume(source_id: str, source_path: Path, payload_path: Path, approval_path: Path, out_dir: Path) -> tuple[Path, Path]:
    source = source_path.read_bytes()
    payload = load_payload(payload_path)
    check(payload, source_id, source)
    run = json.loads(payload_path.with_suffix(".run.json").read_text(encoding="utf-8"))
    payload_hash = digest(canonical(payload))
    if run.get("source_id") != source_id or run.get("source_sha256") != digest(source) or run.get("payload_sha256") != payload_hash or run.get("last_agent") != "Owned-source action extractor" or not str(run.get("run_id", "")).strip():
        raise ValueError("producer receipt does not match accepted candidate")
    approval = json.loads(approval_path.read_text(encoding="utf-8"))
    if (type(approval) is not dict
            or set(approval) != {"approved", "run_id", "source_id", "source_sha256", "payload_sha256", "reviewer", "review_note"}
            or approval["approved"] is not True
            or any(type(approval[key]) is not str for key in ("run_id", "source_id", "source_sha256", "payload_sha256", "reviewer", "review_note"))
            or not approval["reviewer"].strip() or not approval["review_note"].strip()
            or approval["run_id"] != run["run_id"]
            or approval["source_id"] != source_id
            or approval["source_sha256"] != digest(source)
            or approval["payload_sha256"] != payload_hash):
        raise ValueError("human source/meaning approval must bind exact source and payload hashes")
    body = report(payload).encode("utf-8")
    stem = "action-register-" + payload_hash[:16]
    out_dir.mkdir(parents=True, exist_ok=True)
    target = out_dir / (stem + ".md")
    receipt = out_dir / (stem + ".receipt.json")
    record = {"run_id": run["run_id"], "source_id": source_id, "source_sha256": digest(source), "payload_sha256": payload_hash, "report_sha256": digest(body), "reviewer": approval["reviewer"], "operation": "local action-triage register"}
    encoded = (json.dumps(record, indent=2) + "\n").encode("utf-8")
    for path, data in [(target, body), (receipt, encoded)]:
        if path.exists():
            if path.read_bytes() != data:
                raise ValueError(f"uncertain prior output differs: {path}; inspect before retry")
        else:
            with path.open("xb") as handle:
                handle.write(data)
        if path.read_bytes() != data:
            raise IOError(f"readback failed: {path}")
    return target, receipt


def produce(source_id: str, source_path: Path, candidate_path: Path, model: str) -> None:
    from agents import Agent, Runner

    source = source_path.read_bytes()
    source_text = source.decode("utf-8")
    if not source_text.strip() or candidate_path.exists() or not candidate_path.parent.is_dir():
        raise ValueError("source must contain text; candidate must be new and its parent directory must already exist")
    run_id = str(uuid.uuid4())
    agent = Agent(
        name="Owned-source action extractor",
        model=model,
        instructions=("Extract only explicit action commitments from the supplied numbered source. "
                      "Return action-register/v1. Copy an exact quote with matching 1-based line span. "
                      "Use null for an unstated owner or due date; never infer them. "
                      "Use partial and needs_review for ambiguous evidence; blocked for unusable input. "
                      "Do not use tools or hand off."),
        output_type=Extraction,
    )
    numbered = "\n".join(f"{n}: {line}" for n, line in enumerate(source_text.splitlines(), 1))
    try:
        result = Runner.run_sync(agent, f"Source ID: {source_id}\nSHA-256: {digest(source)}\nNumbered source:\n{numbered}")
        if result.interruptions or result.last_agent is not agent or not isinstance(result.final_output, Extraction):
            raise ValueError("run interrupted, unfinished, or finished by unexpected agent; this CLI does not persist resumable RunState, so do not resume from its failure receipt")
        output = result.final_output
        if output.source_id != source_id or output.source_sha256 != digest(source):
            raise ValueError("producer returned wrong source identity")
        candidate = canonical(output)
        with candidate_path.open("xb") as handle:
            handle.write(candidate)  # Candidate only; consumer still requires review.
        receipt = {"run_id": run_id, "source_id": source_id, "source_sha256": digest(source), "payload_sha256": digest(candidate), "last_agent": result.last_agent.name, "model": model}
        with candidate_path.with_suffix(".run.json").open("x", encoding="utf-8") as handle:
            handle.write(json.dumps(receipt, indent=2) + "\n")
    except Exception as exc:
        failure = {"run_id": run_id, "source_id": source_id, "source_sha256": digest(source), "error_type": type(exc).__name__, "error": str(exc), "state": "unknown_or_blocked; inspect run and destination before retry"}
        with candidate_path.with_name(candidate_path.stem + "." + run_id + ".failure.json").open("x", encoding="utf-8") as handle:
            handle.write(json.dumps(failure, indent=2) + "\n")
        raise


def main() -> None:
    parser = argparse.ArgumentParser()
    sub = parser.add_subparsers(dest="command", required=True)
    for name in ("produce", "consume"):
        p = sub.add_parser(name)
        p.add_argument("--source-id", required=True)
        p.add_argument("--source", type=Path, required=True)
        if name == "produce":
            p.add_argument("--candidate", type=Path, required=True)
            p.add_argument("--model", required=True)
        else:
            p.add_argument("--candidate", type=Path, required=True)
            p.add_argument("--approval", type=Path, required=True)
            p.add_argument("--out", type=Path, required=True)
    args = parser.parse_args()
    if args.command == "produce":
        produce(args.source_id, args.source, args.candidate, args.model)
    else:
        print(*consume(args.source_id, args.source, args.candidate, args.approval, args.out), sep="\n")


if __name__ == "__main__":
    main()

Ограничења и извори

Уговор и локални потрошач проверени су ван мреже са пет тестова и седам синтетичких случајева симулираног произвођача или понављања. Обухваћени су строга шема, одбијање пре стварања, груписање, хешеви и грешке. Није било слања шеме пружаоцу, позива модела, издвајања стварних бележака, чињеничног људског одобрења, извештаја из стварног извора ни мерења рачуна или квалитета. API накнаде зависе од налога; проверите компатибилност, цене и дозволу за податке пре слања извора.

За сродан контекст, BIG CHANGE водич за избор AI задатка из сопственог рада покрива претходну одлуку шта испитати. Овај водич почиње изабраним дозвољеним издвајањем и води један типизован резултат до провереног локалног потрошача.

Извори и додатно читање

  • OpenAI Agents SDK: Agents документује Pydantic output_type. Не даје људско одобрење или локалног потрошача из овог поступка.
  • OpenAI Agents SDK: Results документује final_output, last_agent, прекиде и to_state(). Овај CLI не чува настављиво стање.
  • OpenAI Agents SDK: Quickstart обрађује инсталацију и API кључ. Кључ држите ван извора и потврда.
  • OpenAI Structured Outputs објашњава строгу шему и подржане моделе. Потврдите подршку изабраног модела.
  • Pydantic ConfigDict документује строгу валидацију и забрану додатних поља.
  • OpenAI Agents SDK 0.24.0 и Pydantic 2.14.1 означавају тачно везане локално инсталиране верзије. Прихватање код пружаоца није тестирано.