thread-archive 0.0.2__py3-none-any.whl

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.
Files changed (132) hide show
  1. thread_archive/__init__.py +34 -0
  2. thread_archive/_api.py +2235 -0
  3. thread_archive/_config.py +126 -0
  4. thread_archive/_importers/__init__.py +81 -0
  5. thread_archive/_importers/_continuation.py +136 -0
  6. thread_archive/_importers/_cursor.py +103 -0
  7. thread_archive/_importers/_events.py +244 -0
  8. thread_archive/_importers/_line_stream.py +193 -0
  9. thread_archive/_importers/_read.py +82 -0
  10. thread_archive/_importers/_result.py +17 -0
  11. thread_archive/_importers/_sidecar.py +113 -0
  12. thread_archive/_importers/_state.py +240 -0
  13. thread_archive/_importers/_titles.py +179 -0
  14. thread_archive/_importers/antigravity.py +254 -0
  15. thread_archive/_importers/claude_code.py +293 -0
  16. thread_archive/_importers/claude_science.py +330 -0
  17. thread_archive/_importers/cloth.py +20 -0
  18. thread_archive/_importers/codex.py +324 -0
  19. thread_archive/_importers/cowork.py +107 -0
  20. thread_archive/_importers/cursor.py +432 -0
  21. thread_archive/_importers/exports.py +389 -0
  22. thread_archive/_importers/grok.py +600 -0
  23. thread_archive/_importers/opencode.py +538 -0
  24. thread_archive/_knowledge/__init__.py +67 -0
  25. thread_archive/_knowledge/_claims.py +116 -0
  26. thread_archive/_knowledge/_community.py +75 -0
  27. thread_archive/_knowledge/graph.py +208 -0
  28. thread_archive/_knowledge/materialize.py +212 -0
  29. thread_archive/_knowledge/write.py +438 -0
  30. thread_archive/_launchd.py +167 -0
  31. thread_archive/_mcp/__init__.py +1 -0
  32. thread_archive/_mcp/librarian.py +151 -0
  33. thread_archive/_mcp/server.py +307 -0
  34. thread_archive/_retrieval/__init__.py +280 -0
  35. thread_archive/_retrieval/_classify.py +80 -0
  36. thread_archive/_retrieval/_codex.py +157 -0
  37. thread_archive/_retrieval/_context.py +123 -0
  38. thread_archive/_retrieval/_extract.py +184 -0
  39. thread_archive/_retrieval/embed.py +186 -0
  40. thread_archive/_retrieval/format.py +149 -0
  41. thread_archive/_retrieval/fts.py +538 -0
  42. thread_archive/_retrieval/rank.py +228 -0
  43. thread_archive/_retrieval/read.py +908 -0
  44. thread_archive/_retrieval/rerank.py +143 -0
  45. thread_archive/_retrieval/vectors.py +611 -0
  46. thread_archive/_scripts/__init__.py +1 -0
  47. thread_archive/_scripts/backfill_recompute.py +328 -0
  48. thread_archive/_scripts/backfill_reconcile.py +296 -0
  49. thread_archive/_scripts/denamespace_dedup_keys.py +120 -0
  50. thread_archive/_scripts/recover_dropped_events.py +190 -0
  51. thread_archive/_scripts/repair_grok_tool_names.py +118 -0
  52. thread_archive/_scripts/repair_grok_tool_names_backup_20260704T155625Z.json +275 -0
  53. thread_archive/_scripts/repair_grok_tool_names_plan_20260704.json +435 -0
  54. thread_archive/_setup/__init__.py +12 -0
  55. thread_archive/_setup/clients.py +89 -0
  56. thread_archive/_setup/wizard.py +474 -0
  57. thread_archive/_store/__init__.py +45 -0
  58. thread_archive/_store/_base.py +173 -0
  59. thread_archive/_store/_defaults.py +57 -0
  60. thread_archive/_store/_types.py +38 -0
  61. thread_archive/_store/models.py +370 -0
  62. thread_archive/_store/schema.py +84 -0
  63. thread_archive/_thread_import/__init__.py +40 -0
  64. thread_archive/_thread_import/api.py +78 -0
  65. thread_archive/_thread_import/event_builder.py +810 -0
  66. thread_archive/_thread_import/exporters/__init__.py +9 -0
  67. thread_archive/_thread_import/exporters/_cursor_kv_mixin.py +166 -0
  68. thread_archive/_thread_import/exporters/_cursor_parse_mixin.py +149 -0
  69. thread_archive/_thread_import/exporters/cursor.py +582 -0
  70. thread_archive/_thread_import/exporters/cursor_parse.py +145 -0
  71. thread_archive/_thread_import/parsers/__init__.py +191 -0
  72. thread_archive/_thread_import/parsers/base.py +698 -0
  73. thread_archive/_thread_import/parsers/chatgpt.py +722 -0
  74. thread_archive/_thread_import/parsers/chatgpt_content.py +332 -0
  75. thread_archive/_thread_import/parsers/claude.py +443 -0
  76. thread_archive/_thread_import/parsers/claude_code.py +1035 -0
  77. thread_archive/_thread_import/parsers/claude_code_blocks.py +415 -0
  78. thread_archive/_thread_import/parsers/claude_code_ide.py +122 -0
  79. thread_archive/_thread_import/parsers/claude_code_sessions.py +90 -0
  80. thread_archive/_thread_import/parsers/config/__init__.py +26 -0
  81. thread_archive/_thread_import/parsers/config/base.py +220 -0
  82. thread_archive/_thread_import/parsers/cursor.py +538 -0
  83. thread_archive/_thread_import/parsers/cursor_blocks.py +365 -0
  84. thread_archive/_thread_import/parsers/pipeline/__init__.py +29 -0
  85. thread_archive/_thread_import/parsers/pipeline/base.py +251 -0
  86. thread_archive/_thread_import/parsers/pipeline/interfaces.py +191 -0
  87. thread_archive/_thread_import/parsers/transformers/__init__.py +22 -0
  88. thread_archive/_thread_import/parsers/transformers/active_path.py +87 -0
  89. thread_archive/_thread_import/parsers/transformers/coalescing.py +389 -0
  90. thread_archive/_thread_import/parsers/transformers/ide_context.py +158 -0
  91. thread_archive/_thread_import/parsers/transformers/thinking_merge.py +68 -0
  92. thread_archive/_thread_import/parsers/types/__init__.py +56 -0
  93. thread_archive/_thread_import/parsers/types/chatgpt.py +99 -0
  94. thread_archive/_thread_import/parsers/types/claude.py +120 -0
  95. thread_archive/_thread_import/parsers/types/claude_code.py +139 -0
  96. thread_archive/_thread_import/parsers/types/cursor.py +109 -0
  97. thread_archive/_thread_import/parsers/validators/__init__.py +83 -0
  98. thread_archive/_thread_import/parsers/validators/base.py +168 -0
  99. thread_archive/_thread_import/parsers/validators/content.py +80 -0
  100. thread_archive/_thread_import/parsers/validators/referential.py +57 -0
  101. thread_archive/_thread_import/parsers/validators/thinking.py +143 -0
  102. thread_archive/_thread_import/parsers/validators/types.py +87 -0
  103. thread_archive/_thread_import/schemas/__init__.py +231 -0
  104. thread_archive/_thread_import/schemas/chatgpt/v1.json +197 -0
  105. thread_archive/_thread_import/schemas/chatgpt/v2.json +203 -0
  106. thread_archive/_thread_import/schemas/claude/v1.json +188 -0
  107. thread_archive/_thread_import/schemas/claude_code/v1.json +183 -0
  108. thread_archive/_thread_import/schemas/cursor/v1.json +143 -0
  109. thread_archive/_thread_import/schemas/cursor/v2.json +200 -0
  110. thread_archive/_thread_import/timestamps.py +57 -0
  111. thread_archive/_thread_import/tool_names.py +48 -0
  112. thread_archive/_truth/__init__.py +40 -0
  113. thread_archive/_truth/jsonl_log.py +2340 -0
  114. thread_archive/_truth/repair.py +322 -0
  115. thread_archive/_watcher/__init__.py +57 -0
  116. thread_archive/_watcher/base.py +65 -0
  117. thread_archive/_watcher/daemon.py +289 -0
  118. thread_archive/_watcher/export_drop.py +203 -0
  119. thread_archive/_watcher/exthost.py +316 -0
  120. thread_archive/_watcher/lazy.py +117 -0
  121. thread_archive/_watcher/sources.py +616 -0
  122. thread_archive/_web/__init__.py +15 -0
  123. thread_archive/_web/server.py +299 -0
  124. thread_archive/_web/static/assets/index-DECSH1Mz.css +10 -0
  125. thread_archive/_web/static/assets/index-Djq0FK0y.js +101 -0
  126. thread_archive/_web/static/index.html +13 -0
  127. thread_archive/cli.py +746 -0
  128. thread_archive-0.0.2.dist-info/METADATA +296 -0
  129. thread_archive-0.0.2.dist-info/RECORD +132 -0
  130. thread_archive-0.0.2.dist-info/WHEEL +4 -0
  131. thread_archive-0.0.2.dist-info/entry_points.txt +6 -0
  132. thread_archive-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,87 @@
1
+ """
2
+ Type validator.
3
+
4
+ Validates roles and content block types against known values.
5
+ """
6
+
7
+ from typing import List, Set
8
+
9
+ from ..base import NormalizedMessage
10
+ from .base import BaseValidator, ValidationContext, ValidationSeverity
11
+
12
+ # Universal known block types across all providers
13
+ KNOWN_BLOCK_TYPES: Set[str] = {
14
+ "text",
15
+ "thinking",
16
+ "tool_use",
17
+ "tool_result",
18
+ "image",
19
+ "file",
20
+ "code",
21
+ "system_context",
22
+ "parse_error",
23
+ "file_snapshot",
24
+ "context_summary",
25
+ "system_metadata",
26
+ "ide_context",
27
+ "queue_operation",
28
+ "progress",
29
+ }
30
+
31
+
32
+ class TypeValidator(BaseValidator):
33
+ """Validates message roles and content block types.
34
+
35
+ Unknown types get warnings (not errors) because:
36
+ - Provider may have added new features
37
+ - We want to preserve unknown data, not reject it
38
+
39
+ This helps detect format changes in provider exports.
40
+ """
41
+
42
+ def validate(
43
+ self, messages: List[NormalizedMessage], context: ValidationContext
44
+ ) -> None:
45
+ """Validate types across all messages."""
46
+ unknown_roles: Set[str] = set()
47
+ unknown_block_types: Set[str] = set()
48
+
49
+ for msg in messages:
50
+ role = msg.get("role", "")
51
+
52
+ # Check role against provider's expected roles
53
+ if role and self.config.expected_roles:
54
+ if role not in self.config.expected_roles:
55
+ unknown_roles.add(role)
56
+
57
+ # Check content block types
58
+ for block in msg.get("content_blocks", []):
59
+ block_type = block.get("type", "")
60
+ if block_type:
61
+ # Check against universal known types
62
+ if block_type not in KNOWN_BLOCK_TYPES:
63
+ unknown_block_types.add(block_type)
64
+ # Optionally check against provider-specific expected types
65
+ elif (
66
+ self.config.expected_block_types
67
+ and block_type not in self.config.expected_block_types
68
+ ):
69
+ # Provider has restricted set - warn about unexpected
70
+ unknown_block_types.add(block_type)
71
+
72
+ # Report unknown types
73
+ for role in sorted(unknown_roles):
74
+ self._add_issue(
75
+ context,
76
+ f"Unknown message role '{role}' from provider {self.config.provider_name} - "
77
+ f"possible format change or new feature",
78
+ ValidationSeverity.warning,
79
+ )
80
+
81
+ for block_type in sorted(unknown_block_types):
82
+ self._add_issue(
83
+ context,
84
+ f"Unknown content block type '{block_type}' - "
85
+ f"possible format change or new feature",
86
+ ValidationSeverity.warning,
87
+ )
@@ -0,0 +1,231 @@
1
+ """
2
+ JSON Schema validation for provider export formats.
3
+
4
+ Provides versioned schema validation with automatic version detection.
5
+ Schemas are stored as JSON files in provider-specific subdirectories.
6
+
7
+ Usage:
8
+ from apps.chat_import.schemas import validate_export, detect_schema_version
9
+
10
+ # Auto-detect and validate
11
+ errors, warnings = validate_export(data, provider="chatgpt")
12
+
13
+ # Or detect version explicitly
14
+ version = detect_schema_version(data, provider="chatgpt")
15
+ errors, warnings = validate_export(data, provider="chatgpt", version=version)
16
+ """
17
+
18
+ import json
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional, Tuple
21
+
22
+ try:
23
+ import jsonschema
24
+ HAS_JSONSCHEMA = True
25
+ except ImportError:
26
+ HAS_JSONSCHEMA = False
27
+
28
+
29
+ SCHEMA_DIR = Path(__file__).parent
30
+
31
+
32
+ def get_available_versions(provider: str) -> List[str]:
33
+ """Get list of available schema versions for a provider."""
34
+ provider_dir = SCHEMA_DIR / provider
35
+ if not provider_dir.exists():
36
+ return []
37
+
38
+ versions = []
39
+ for schema_file in provider_dir.glob("v*.json"):
40
+ versions.append(schema_file.stem) # e.g., "v1", "v2"
41
+
42
+ return sorted(versions)
43
+
44
+
45
+ def load_schema(provider: str, version: str) -> Optional[Dict[str, Any]]:
46
+ """Load a specific schema version."""
47
+ schema_path = SCHEMA_DIR / provider / f"{version}.json"
48
+ if not schema_path.exists():
49
+ return None
50
+
51
+ with open(schema_path) as f:
52
+ return json.load(f)
53
+
54
+
55
+ def detect_schema_version(data: Any, provider: str) -> str:
56
+ """
57
+ Auto-detect schema version from data structure.
58
+
59
+ Uses heuristics specific to each provider to determine which
60
+ schema version the data matches.
61
+
62
+ Returns the detected version string (e.g., "v1", "v2") or "v1" as default.
63
+ """
64
+ if provider == "chatgpt":
65
+ return _detect_chatgpt_version(data)
66
+ elif provider == "claude":
67
+ return _detect_claude_version(data)
68
+ elif provider == "claude-code":
69
+ return _detect_claude_code_version(data)
70
+ elif provider == "cursor":
71
+ return _detect_cursor_version(data)
72
+
73
+ return "v1"
74
+
75
+
76
+ def _detect_chatgpt_version(data: Any) -> str:
77
+ """Detect ChatGPT export version.
78
+
79
+ v2: Has thinking blocks (content_type: "thoughts")
80
+ v1: Original format
81
+ """
82
+ if not isinstance(data, list):
83
+ data = [data]
84
+
85
+ for conv in data:
86
+ mapping = conv.get("mapping", {})
87
+ for node in mapping.values():
88
+ msg = node.get("message")
89
+ if msg:
90
+ metadata = msg.get("metadata", {})
91
+ content_type = metadata.get("content_type")
92
+ if content_type in ("thoughts", "analysis", "reasoning_recap"):
93
+ return "v2"
94
+
95
+ return "v1"
96
+
97
+
98
+ def _detect_claude_version(data: Any) -> str:
99
+ """Detect Claude export version.
100
+
101
+ v1: Current format (only version so far)
102
+ """
103
+ return "v1"
104
+
105
+
106
+ def _detect_claude_code_version(data: Any) -> str:
107
+ """Detect Claude Code session version.
108
+
109
+ v1: Current JSONL format (only version so far)
110
+ """
111
+ return "v1"
112
+
113
+
114
+ def _detect_cursor_version(data: Any) -> str:
115
+ """Detect Cursor export version.
116
+
117
+ v2: Has toolFormerData field
118
+ v1: Original format
119
+ """
120
+ if isinstance(data, dict):
121
+ bubbles = data.get("bubbles", [])
122
+ elif isinstance(data, list):
123
+ bubbles = data
124
+ else:
125
+ return "v1"
126
+
127
+ for bubble in bubbles:
128
+ if bubble.get("toolFormerData"):
129
+ return "v2"
130
+
131
+ return "v1"
132
+
133
+
134
+ def validate_export(
135
+ data: Any,
136
+ provider: str,
137
+ version: Optional[str] = None,
138
+ ) -> Tuple[List[str], List[str]]:
139
+ """
140
+ Validate export data against JSON Schema.
141
+
142
+ Args:
143
+ data: The export data to validate
144
+ provider: Provider name (chatgpt, claude, claude-code, cursor)
145
+ version: Schema version (auto-detected if not specified)
146
+
147
+ Returns:
148
+ Tuple of (errors, warnings) where:
149
+ - errors: List of validation error messages
150
+ - warnings: List of validation warning messages (unknown fields, etc.)
151
+ """
152
+ if not HAS_JSONSCHEMA:
153
+ # Return empty if jsonschema not installed
154
+ return [], ["jsonschema library not installed, skipping validation"]
155
+
156
+ if version is None:
157
+ version = detect_schema_version(data, provider)
158
+
159
+ schema = load_schema(provider, version)
160
+ if schema is None:
161
+ # No schema file for this version - not an error, just a warning
162
+ available = get_available_versions(provider)
163
+ if available:
164
+ return [], [f"No schema for {provider} {version}, available: {available}"]
165
+ else:
166
+ return [], [f"No schemas defined for provider {provider}"]
167
+
168
+ errors: List[str] = []
169
+ warnings: List[str] = []
170
+
171
+ try:
172
+ # Validate against schema
173
+ validator = jsonschema.Draft7Validator(schema)
174
+
175
+ for error in validator.iter_errors(data):
176
+ # Categorize errors
177
+ if error.validator == "additionalProperties":
178
+ # Unknown field - warning not error
179
+ warnings.append(f"Unknown field at {_format_path(error.path)}: {error.message}")
180
+ elif error.validator == "required":
181
+ # Missing required field - error
182
+ errors.append(f"Missing required field at {_format_path(error.path)}: {error.message}")
183
+ elif error.validator == "type":
184
+ # Type mismatch - error
185
+ errors.append(f"Type mismatch at {_format_path(error.path)}: {error.message}")
186
+ else:
187
+ # Other validation errors
188
+ errors.append(f"Validation error at {_format_path(error.path)}: {error.message}")
189
+
190
+ except Exception as e:
191
+ errors.append(f"Schema validation failed: {str(e)}")
192
+
193
+ return errors, warnings
194
+
195
+
196
+ def _format_path(path) -> str:
197
+ """Format a JSON path for display."""
198
+ if not path:
199
+ return "root"
200
+ parts = []
201
+ for p in path:
202
+ if isinstance(p, int):
203
+ parts.append(f"[{p}]")
204
+ else:
205
+ parts.append(f".{p}")
206
+ return "".join(parts).lstrip(".")
207
+
208
+
209
+ def get_schema_violations(
210
+ data: Any,
211
+ provider: str,
212
+ version: Optional[str] = None,
213
+ ) -> Dict[str, List[str]]:
214
+ """
215
+ Get detailed schema violations.
216
+
217
+ Returns:
218
+ Dict with keys:
219
+ - unknown_fields: Fields not in schema
220
+ - missing_fields: Required fields that are missing
221
+ - type_mismatches: Fields with wrong types
222
+ - other_errors: Other validation errors
223
+ """
224
+ errors, warnings = validate_export(data, provider, version)
225
+
226
+ return {
227
+ "unknown_fields": [w for w in warnings if "Unknown field" in w],
228
+ "missing_fields": [e for e in errors if "Missing required" in e],
229
+ "type_mismatches": [e for e in errors if "Type mismatch" in e],
230
+ "other_errors": [e for e in errors if "Missing required" not in e and "Type mismatch" not in e],
231
+ }
@@ -0,0 +1,197 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "chatgpt_export_v1",
4
+ "title": "ChatGPT Export v1",
5
+ "description": "Original ChatGPT conversation export format",
6
+ "type": "array",
7
+ "items": {
8
+ "$ref": "#/definitions/conversation"
9
+ },
10
+ "definitions": {
11
+ "conversation": {
12
+ "type": "object",
13
+ "required": ["title", "mapping"],
14
+ "properties": {
15
+ "title": {
16
+ "type": ["string", "null"],
17
+ "description": "Conversation title"
18
+ },
19
+ "create_time": {
20
+ "type": ["number", "null"],
21
+ "description": "Unix timestamp when conversation was created"
22
+ },
23
+ "update_time": {
24
+ "type": ["number", "null"],
25
+ "description": "Unix timestamp when conversation was last updated"
26
+ },
27
+ "mapping": {
28
+ "type": "object",
29
+ "additionalProperties": {
30
+ "$ref": "#/definitions/mappingNode"
31
+ },
32
+ "description": "Tree structure of messages"
33
+ },
34
+ "current_node": {
35
+ "type": ["string", "null"],
36
+ "description": "ID of the currently active message node"
37
+ },
38
+ "conversation_template_id": {
39
+ "type": ["string", "null"]
40
+ },
41
+ "gizmo_id": {
42
+ "type": ["string", "null"]
43
+ },
44
+ "id": {
45
+ "type": "string"
46
+ },
47
+ "moderation_results": {
48
+ "type": "array"
49
+ },
50
+ "safe_urls": {
51
+ "type": "array"
52
+ }
53
+ }
54
+ },
55
+ "mappingNode": {
56
+ "type": "object",
57
+ "properties": {
58
+ "id": {
59
+ "type": "string",
60
+ "description": "Node/message ID"
61
+ },
62
+ "parent": {
63
+ "type": ["string", "null"],
64
+ "description": "Parent node ID (null for root)"
65
+ },
66
+ "children": {
67
+ "type": "array",
68
+ "items": {
69
+ "type": "string"
70
+ },
71
+ "description": "Child node IDs"
72
+ },
73
+ "message": {
74
+ "oneOf": [
75
+ {"$ref": "#/definitions/message"},
76
+ {"type": "null"}
77
+ ],
78
+ "description": "Message content (null for structural nodes)"
79
+ }
80
+ }
81
+ },
82
+ "message": {
83
+ "type": "object",
84
+ "required": ["id", "author", "content"],
85
+ "properties": {
86
+ "id": {
87
+ "type": "string",
88
+ "description": "Message ID"
89
+ },
90
+ "author": {
91
+ "$ref": "#/definitions/author"
92
+ },
93
+ "content": {
94
+ "$ref": "#/definitions/content"
95
+ },
96
+ "create_time": {
97
+ "type": ["number", "null"],
98
+ "description": "Unix timestamp when message was created"
99
+ },
100
+ "update_time": {
101
+ "type": ["number", "null"]
102
+ },
103
+ "status": {
104
+ "type": "string",
105
+ "enum": ["finished_successfully", "in_progress", "finished_partial_completion"]
106
+ },
107
+ "end_turn": {
108
+ "type": ["boolean", "null"]
109
+ },
110
+ "weight": {
111
+ "type": "number"
112
+ },
113
+ "metadata": {
114
+ "$ref": "#/definitions/metadata"
115
+ },
116
+ "recipient": {
117
+ "type": "string"
118
+ }
119
+ }
120
+ },
121
+ "author": {
122
+ "type": "object",
123
+ "required": ["role"],
124
+ "properties": {
125
+ "role": {
126
+ "type": "string",
127
+ "enum": ["user", "assistant", "system", "tool"],
128
+ "description": "Message author role"
129
+ },
130
+ "name": {
131
+ "type": ["string", "null"]
132
+ },
133
+ "metadata": {
134
+ "type": "object"
135
+ }
136
+ }
137
+ },
138
+ "content": {
139
+ "type": "object",
140
+ "properties": {
141
+ "content_type": {
142
+ "type": "string",
143
+ "enum": ["text", "code", "execution_output", "tether_browsing_display", "tether_quote", "multimodal_text", "system_error"]
144
+ },
145
+ "parts": {
146
+ "type": "array",
147
+ "description": "Content parts (text strings or objects)"
148
+ },
149
+ "language": {
150
+ "type": "string"
151
+ },
152
+ "text": {
153
+ "type": "string"
154
+ },
155
+ "result": {
156
+ "type": "string"
157
+ }
158
+ }
159
+ },
160
+ "metadata": {
161
+ "type": "object",
162
+ "properties": {
163
+ "is_user_system_message": {
164
+ "type": "boolean",
165
+ "description": "True if this is user's custom instructions"
166
+ },
167
+ "user_context_message_data": {
168
+ "type": "object"
169
+ },
170
+ "model_slug": {
171
+ "type": "string"
172
+ },
173
+ "default_model_slug": {
174
+ "type": "string"
175
+ },
176
+ "finish_details": {
177
+ "type": "object"
178
+ },
179
+ "citations": {
180
+ "type": "array"
181
+ },
182
+ "gizmo_id": {
183
+ "type": ["string", "null"]
184
+ },
185
+ "message_type": {
186
+ "type": ["string", "null"]
187
+ },
188
+ "request_id": {
189
+ "type": "string"
190
+ },
191
+ "timestamp_": {
192
+ "type": "string"
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }