Skill Tracer
Skills › Scientific Agent Skills › open-notebook › examples.md

examples.md

182 lines · skills/open-notebook/references/examples.md at 5f2bc03

Open on GitHub
1# Open Notebook Examples
2
3These examples target the official **v1.14.0** contracts documented in
4[the API reference](api_reference.md). Run from this skill's `scripts/` directory
5with `requests` installed and `OPEN_NOTEBOOK_URL` pointing to the backend.
6The shared client adds `OPEN_NOTEBOOK_PASSWORD` as a Bearer token when set,
7checks HTTP errors, and uses finite timeouts. Examples are illustrative for a live
8instance; mocked tests verify request bodies, pagination, status handling, and
9response parsing without performing real ingestion or model calls.

Open Notebook Examples

These examples target the official v1.14.0 contracts documented in the API reference. Run from this skill's scripts/ directory with requests installed and OPEN_NOTEBOOK_URL pointing to the backend. The shared client adds OPEN_NOTEBOOK_PASSWORD as a Bearer token when set, checks HTTP errors, and uses finite timeouts. Examples are illustrative for a live instance; mocked tests verify request bodies, pagination, status handling, and response parsing without performing real ingestion or model calls.

Raw lines 1 to 10
11## Research workflow with explicit context
13The caller supplies an accessible source URL. Respect access rights; an abstract
14page or a paywall response is not the full paper. Set `embed=True` only when a
15configured embedding model should process the text. This example uses full text
16for chat and keyword search, so it does not request embeddings.
18```python
19from _common import record_path, request_json
20from notebook_management import create_notebook
21from source_ingestion import add_url_source, wait_for_processing
22from chat_interaction import build_context, create_chat_session, send_chat_message
25def research_workflow(source_url):
26 notebook = create_notebook("Study design review", "Inspect primary methods evidence")
27 source = add_url_source(notebook["id"], source_url, embed=False)
28 wait_for_processing(source["id"])
29 extracted = request_json("GET", "/sources/" + record_path(source["id"]))
30 if not (extracted.get("full_text") or "").strip():
31 raise RuntimeError("Extraction produced no research text")
32 built = build_context(notebook["id"], source_ids=[source["id"]], note_ids=[])
33 selected = built["context"]["sources"]
34 if not selected or not any(item.get("full_text") for item in selected):
35 raise RuntimeError("Full source text was not included in chat context")
36 session = create_chat_session(notebook["id"], "Methods review")
37 answer = send_chat_message(
38 session["id"], "Extract the study design and sample size; cite the source. "
39 "State what is missing and distinguish reported facts from inference.",
40 built["context"],
41 )
42 ai_messages = [m for m in answer["messages"] if m["type"] == "ai"]
43 if not ai_messages:
44 raise RuntimeError("Chat returned no AI message")
45 note = request_json("POST", "/notes", json={
46 "notebook_id": notebook["id"], "title": "Methods extraction - unverified draft",
47 "note_type": "ai", "content": ai_messages[-1]["content"],
48 })
49 return {"notebook": notebook, "source": extracted, "note": note,
50 "context_tokens": built["token_count"]}
51```
53Review source extraction and citations before interpreting the note as evidence.
54For sensitive projects, inspect the assembled context before invoking chat. Keep
55records after the workflow for provenance; cleanup is an explicit later action.

Research workflow with explicit context

The caller supplies an accessible source URL. Respect access rights; an abstract page or a paywall response is not the full paper. Set embed=True only when a configured embedding model should process the text. This example uses full text for chat and keyword search, so it does not request embeddings.

from _common import record_path, request_json
from notebook_management import create_notebook
from source_ingestion import add_url_source, wait_for_processing
from chat_interaction import build_context, create_chat_session, send_chat_message


def research_workflow(source_url):
    notebook = create_notebook("Study design review", "Inspect primary methods evidence")
    source = add_url_source(notebook["id"], source_url, embed=False)
    wait_for_processing(source["id"])
    extracted = request_json("GET", "/sources/" + record_path(source["id"]))
    if not (extracted.get("full_text") or "").strip():
        raise RuntimeError("Extraction produced no research text")
    built = build_context(notebook["id"], source_ids=[source["id"]], note_ids=[])
    selected = built["context"]["sources"]
    if not selected or not any(item.get("full_text") for item in selected):
        raise RuntimeError("Full source text was not included in chat context")
    session = create_chat_session(notebook["id"], "Methods review")
    answer = send_chat_message(
        session["id"], "Extract the study design and sample size; cite the source. "
        "State what is missing and distinguish reported facts from inference.",
        built["context"],
    )
    ai_messages = [m for m in answer["messages"] if m["type"] == "ai"]
    if not ai_messages:
        raise RuntimeError("Chat returned no AI message")
    note = request_json("POST", "/notes", json={
        "notebook_id": notebook["id"], "title": "Methods extraction - unverified draft",
        "note_type": "ai", "content": ai_messages[-1]["content"],
    })
    return {"notebook": notebook, "source": extracted, "note": note,
            "context_tokens": built["token_count"]}

Review source extraction and citations before interpreting the note as evidence. For sensitive projects, inspect the assembled context before invoking chat. Keep records after the workflow for provenance; cleanup is an explicit later action.

Raw lines 11 to 56
57## Uploads and complete source listing
59```python
60from source_ingestion import upload_file_source, wait_for_processing, iter_sources
63def upload_papers(notebook_id, file_paths):
64 uploaded = []
65 for path in file_paths:
66 source = upload_file_source(notebook_id, path, process_async=True, embed=False)
67 wait_for_processing(source["id"])
68 uploaded.append(source["id"])
69 return uploaded
72def all_notebook_sources(notebook_id):
73 return list(iter_sources(notebook_id, page_size=100))
74```
76The upload helper sends `type=upload` and multipart bytes; it never assumes that
77a client file path exists on the server. Long audio/video or OCR may require a
78longer polling deadline and optional extraction dependencies. A completed source
79can still lack embeddings; inspect `embedded_chunks` before vector retrieval.

Uploads and complete source listing

from source_ingestion import upload_file_source, wait_for_processing, iter_sources


def upload_papers(notebook_id, file_paths):
    uploaded = []
    for path in file_paths:
        source = upload_file_source(notebook_id, path, process_async=True, embed=False)
        wait_for_processing(source["id"])
        uploaded.append(source["id"])
    return uploaded


def all_notebook_sources(notebook_id):
    return list(iter_sources(notebook_id, page_size=100))

The upload helper sends type=upload and multipart bytes; it never assumes that a client file path exists on the server. Long audio/video or OCR may require a longer polling deadline and optional extraction dependencies. A completed source can still lack embeddings; inspect embedded_chunks before vector retrieval.

Raw lines 57 to 80
81## Search and Ask
83```python
84from _common import request_json
85from chat_interaction import search_knowledge_base, ask_question
88def search_and_answer(query):
89 results = search_knowledge_base(query, search_type="vector", limit=10)
90 defaults = request_json("GET", "/models/defaults")
91 model_id = defaults.get("default_chat_model")
92 if not model_id or not defaults.get("default_embedding_model"):
93 raise RuntimeError("Configure chat and embedding models first")
94 answer = ask_question(query, model_id, model_id, model_id)
95 return results["total_count"], results["results"], answer["answer"]
96```
98This searches **all** eligible materials on v1.14.0. The Ask call performs its own
99retrieval; it does not synthesize only the ten preceding search results. Do not
100pass `source_ids` and claim source-restricted retrieval. Main after the release
101adds notebook scoping, but it must be verified in the installed OpenAPI schema.
102Select explicit source context for a bounded notebook chat on v1.14.0 instead.

Search and Ask

from _common import request_json
from chat_interaction import search_knowledge_base, ask_question


def search_and_answer(query):
    results = search_knowledge_base(query, search_type="vector", limit=10)
    defaults = request_json("GET", "/models/defaults")
    model_id = defaults.get("default_chat_model")
    if not model_id or not defaults.get("default_embedding_model"):
        raise RuntimeError("Configure chat and embedding models first")
    answer = ask_question(query, model_id, model_id, model_id)
    return results["total_count"], results["results"], answer["answer"]

This searches all eligible materials on v1.14.0. The Ask call performs its own retrieval; it does not synthesize only the ten preceding search results. Do not pass source_ids and claim source-restricted retrieval. Main after the release adds notebook scoping, but it must be verified in the installed OpenAPI schema. Select explicit source context for a bounded notebook chat on v1.14.0 instead.

Raw lines 81 to 103
104## Custom transformation
106```python
107from _common import request_json
110def extract_methods(text, model_id):
111 transformation = request_json("POST", "/transformations", json={
112 "name": "review_methods", "title": "Review Methods",
113 "description": "Extract reported methods with provenance and omissions",
114 "prompt": "Extract study design, sample size, variables and statistical "
115 "methods. Quote short supporting passages. Mark missing details; "
116 "do not invent values or infer a design from the conclusions.",
117 "apply_default": False,
118 })
119 result = request_json("POST", "/transformations/execute", json={
120 "transformation_id": transformation["id"], "input_text": text,
121 "model_id": model_id,
122 })
123 return result["output"]
124```
126Select `model_id` from `GET /api/models?type=language`, matching a suitable registered
127model. Execution returns text without automatically persisting a note. Record the
128source/model/transformation IDs with any saved analysis and verify quantitative
129claims against original tables, figures, and units.

Custom transformation

from _common import request_json


def extract_methods(text, model_id):
    transformation = request_json("POST", "/transformations", json={
        "name": "review_methods", "title": "Review Methods",
        "description": "Extract reported methods with provenance and omissions",
        "prompt": "Extract study design, sample size, variables and statistical "
                  "methods. Quote short supporting passages. Mark missing details; "
                  "do not invent values or infer a design from the conclusions.",
        "apply_default": False,
    })
    result = request_json("POST", "/transformations/execute", json={
        "transformation_id": transformation["id"], "input_text": text,
        "model_id": model_id,
    })
    return result["output"]

Select model_id from GET /api/models?type=language, matching a suitable registered model. Execution returns text without automatically persisting a note. Record the source/model/transformation IDs with any saved analysis and verify quantitative claims against original tables, figures, and units.

Raw lines 104 to 130
131## Podcast generation from reviewed text
133Choose names from `GET /api/episode-profiles` and `GET /api/speaker-profiles`.
134One speaker profile contains the cast; the API does not accept a speaker ID list.
135The profile's language and speech models must already be configured. This operation
136can incur provider charges.
138```python
139from pathlib import Path
140import time
141from _common import record_path, request, request_json
144def generate_podcast(content, episode_profile, speaker_profile, output_path,
145 timeout=1800):
146 if timeout <= 0:
147 raise ValueError("timeout must be positive")
148 job = request_json("POST", "/podcasts/generate", json={
149 "episode_profile": episode_profile, "speaker_profile": speaker_profile,
150 "episode_name": "Reviewed methods discussion", "content": content,
151 })
152 deadline = time.monotonic() + timeout
153 while True:
154 remaining = deadline - time.monotonic()
155 if remaining <= 0:
156 raise TimeoutError(f"Podcast job {job['job_id']} still pending")
157 status = request_json("GET", "/podcasts/jobs/" + record_path(job["job_id"]),
158 timeout=min(30, remaining))
159 if status["status"] in {"failed", "error", "canceled", "cancelled"}:
160 raise RuntimeError(status.get("error_message") or "Podcast generation failed")
161 if status["status"] == "completed":
162 episode_id = (status.get("result") or {}).get("episode_id")
163 if not episode_id:
164 raise RuntimeError("Completed job returned no episode_id")
165 break
166 time.sleep(min(5, max(0, deadline - time.monotonic())))
167 audio_path = "/podcasts/episodes/" + record_path(episode_id) + "/audio"
168 with request("GET", audio_path, stream=True) as response:
169 content_type = response.headers.get("Content-Type", "").split(";", 1)[0]
170 if content_type != "audio/mpeg":
171 raise RuntimeError(f"Unexpected podcast content type: {content_type}")
172 with Path(output_path).open("xb") as handle:
173 for chunk in response.iter_content(chunk_size=65536):
174 if chunk:
175 handle.write(chunk)
176 return episode_id
177```
179Use a new output filename. A download interrupted after writing begins leaves a
180partial file; inspect it before reuse. Fetch the episode and review its transcript
181and scientific claims before distributing the audio. A timed-out generation request
182may still run on the server—inspect jobs/episodes rather than submitting duplicates.

Podcast generation from reviewed text

Choose names from GET /api/episode-profiles and GET /api/speaker-profiles. One speaker profile contains the cast; the API does not accept a speaker ID list. The profile's language and speech models must already be configured. This operation can incur provider charges.

from pathlib import Path
import time
from _common import record_path, request, request_json


def generate_podcast(content, episode_profile, speaker_profile, output_path,
                     timeout=1800):
    if timeout <= 0:
        raise ValueError("timeout must be positive")
    job = request_json("POST", "/podcasts/generate", json={
        "episode_profile": episode_profile, "speaker_profile": speaker_profile,
        "episode_name": "Reviewed methods discussion", "content": content,
    })
    deadline = time.monotonic() + timeout
    while True:
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise TimeoutError(f"Podcast job {job['job_id']} still pending")
        status = request_json("GET", "/podcasts/jobs/" + record_path(job["job_id"]),
                              timeout=min(30, remaining))
        if status["status"] in {"failed", "error", "canceled", "cancelled"}:
            raise RuntimeError(status.get("error_message") or "Podcast generation failed")
        if status["status"] == "completed":
            episode_id = (status.get("result") or {}).get("episode_id")
            if not episode_id:
                raise RuntimeError("Completed job returned no episode_id")
            break
        time.sleep(min(5, max(0, deadline - time.monotonic())))
    audio_path = "/podcasts/episodes/" + record_path(episode_id) + "/audio"
    with request("GET", audio_path, stream=True) as response:
        content_type = response.headers.get("Content-Type", "").split(";", 1)[0]
        if content_type != "audio/mpeg":
            raise RuntimeError(f"Unexpected podcast content type: {content_type}")
        with Path(output_path).open("xb") as handle:
            for chunk in response.iter_content(chunk_size=65536):
                if chunk:
                    handle.write(chunk)
    return episode_id

Use a new output filename. A download interrupted after writing begins leaves a partial file; inspect it before reuse. Fetch the episode and review its transcript and scientific claims before distributing the audio. A timed-out generation request may still run on the server—inspect jobs/episodes rather than submitting duplicates.

Raw lines 131 to 182