Extracts per-message content into a typed `blocks` list (text, code, thinking, tool_use, tool_result, image_placeholder, file_placeholder, unknown) and renders them at exporter write time. Voice transcripts, Custom Instructions, and image references now appear in exports instead of being silently dropped. Foundation: - src/blocks.py: pure block constructors, _safe_fence (fence-corruption defense, verified live in Joplin), _blockquote_prefix, render - src/loss_report.py: per-run tally surfaced as INFO summary at end of export so silently-dropped data becomes visible Providers: - ChatGPT: dispatch on content_type produces typed blocks; voice shapes (audio_transcription, audio_asset_pointer, real_time_user_audio_video_ asset_pointer) locked from live DevTools capture; Custom Instructions bug fix (parts-vs-direct-fields); role filter lifted; hidden-context marker driven by is_visually_hidden_from_conversation flag - Claude: defensive dispatch for text/thinking/tool_use/tool_result/image with recursive nested-block flattening; untested against real rich- content data — fix-forward in v0.4.1 Exporter: - Markdown renders from blocks at write time via render_blocks_to_markdown; backward-compat fallback to content for any pre-v0.4.0 cached data Tests: - 27 new tests across providers, exporters, CLI; fixtures rebuilt with real-shape ChatGPT voice + Custom Instructions cases - 181/181 pass Behavior changes (intentional): - JSON output omits content; consumers should read blocks - Per-conversation message counts increase (Custom Instructions, image- only, tool-only messages now appear) - Existing exports not auto-re-rendered; users wanting fresh output run cache --clear then export Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
130 lines
4.6 KiB
JSON
130 lines
4.6 KiB
JSON
{
|
|
"id": "chatgpt-conv-001",
|
|
"title": "Python Async Tutorial",
|
|
"create_time": 1704067200.0,
|
|
"update_time": 1704153600.0,
|
|
"project_title": "Learning Python",
|
|
"mapping": {
|
|
"node-root": {
|
|
"id": "node-root",
|
|
"parent": null,
|
|
"children": ["node-uec"],
|
|
"message": null
|
|
},
|
|
"node-uec": {
|
|
"id": "node-uec",
|
|
"parent": "node-root",
|
|
"children": ["node-1"],
|
|
"message": {
|
|
"id": "node-uec",
|
|
"author": {"role": "user"},
|
|
"create_time": null,
|
|
"content": {
|
|
"content_type": "user_editable_context",
|
|
"user_profile": "Preferred name: Jesse",
|
|
"user_instructions": "The user provided the additional info about how they would like you to respond:\n```Always cite sources.```"
|
|
},
|
|
"metadata": {
|
|
"is_visually_hidden_from_conversation": true
|
|
}
|
|
}
|
|
},
|
|
"node-1": {
|
|
"id": "node-1",
|
|
"parent": "node-uec",
|
|
"children": ["node-2"],
|
|
"message": {
|
|
"id": "node-1",
|
|
"author": {"role": "user"},
|
|
"create_time": 1704067200.0,
|
|
"content": {
|
|
"content_type": "text",
|
|
"parts": ["How does async/await work in Python?"]
|
|
}
|
|
}
|
|
},
|
|
"node-2": {
|
|
"id": "node-2",
|
|
"parent": "node-1",
|
|
"children": ["node-mm-user"],
|
|
"message": {
|
|
"id": "node-2",
|
|
"author": {"role": "assistant"},
|
|
"create_time": 1704067210.0,
|
|
"content": {
|
|
"content_type": "text",
|
|
"parts": ["Async/await in Python allows you to write concurrent code using coroutines.\n\n```python\nimport asyncio\n\nasync def main():\n await asyncio.sleep(1)\n print('Done!')\n\nasyncio.run(main())\n```"]
|
|
}
|
|
}
|
|
},
|
|
"node-mm-user": {
|
|
"id": "node-mm-user",
|
|
"parent": "node-2",
|
|
"children": ["node-mm-assistant"],
|
|
"message": {
|
|
"id": "node-mm-user",
|
|
"author": {"role": "user"},
|
|
"create_time": 1704067300.0,
|
|
"content": {
|
|
"content_type": "multimodal_text",
|
|
"parts": [
|
|
{"content_type": "audio_transcription", "text": "What is the capital of France?", "direction": "in", "decoding_id": null},
|
|
{"content_type": "real_time_user_audio_video_asset_pointer", "frames_asset_pointers": [], "video_container_asset_pointer": null, "audio_asset_pointer": {"content_type": "audio_asset_pointer", "asset_pointer": "sediment://file_user001", "size_bytes": 50000, "format": "wav", "metadata": {"start": 0.0, "end": 2.5}}, "audio_start_timestamp": 1.0}
|
|
]
|
|
},
|
|
"metadata": {"voice_mode_message": true}
|
|
}
|
|
},
|
|
"node-mm-assistant": {
|
|
"id": "node-mm-assistant",
|
|
"parent": "node-mm-user",
|
|
"children": ["node-mm-user-rev"],
|
|
"message": {
|
|
"id": "node-mm-assistant",
|
|
"author": {"role": "assistant"},
|
|
"create_time": 1704067305.0,
|
|
"content": {
|
|
"content_type": "multimodal_text",
|
|
"parts": [
|
|
{"content_type": "audio_transcription", "text": "The capital of France is Paris.", "direction": "out", "decoding_id": null},
|
|
{"content_type": "audio_asset_pointer", "asset_pointer": "sediment://file_assistant001", "size_bytes": 80000, "format": "wav", "metadata": {"start": 0.0, "end": 3.2}}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"node-mm-user-rev": {
|
|
"id": "node-mm-user-rev",
|
|
"parent": "node-mm-assistant",
|
|
"children": ["node-image-only"],
|
|
"message": {
|
|
"id": "node-mm-user-rev",
|
|
"author": {"role": "user"},
|
|
"create_time": 1704067400.0,
|
|
"content": {
|
|
"content_type": "multimodal_text",
|
|
"parts": [
|
|
{"content_type": "real_time_user_audio_video_asset_pointer", "frames_asset_pointers": [], "video_container_asset_pointer": null, "audio_asset_pointer": {"content_type": "audio_asset_pointer", "asset_pointer": "sediment://file_user002", "size_bytes": 30000, "format": "wav", "metadata": {"start": 0.0, "end": 1.5}}, "audio_start_timestamp": 5.0},
|
|
{"content_type": "audio_transcription", "text": "Tell me more please.", "direction": "in", "decoding_id": null}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
"node-image-only": {
|
|
"id": "node-image-only",
|
|
"parent": "node-mm-user-rev",
|
|
"children": [],
|
|
"message": {
|
|
"id": "node-image-only",
|
|
"author": {"role": "user"},
|
|
"create_time": 1704067500.0,
|
|
"content": {
|
|
"content_type": "multimodal_text",
|
|
"parts": [
|
|
{"content_type": "image_asset_pointer", "asset_pointer": "file-service://image001"}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|