nardial 0.1.0__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.
- nardial/__init__.py +0 -0
- nardial/authoring/__init__.py +1 -0
- nardial/authoring/export_html.py +215 -0
- nardial/authoring/factory.py +178 -0
- nardial/authoring/loader.py +70 -0
- nardial/conversation_agent.py +305 -0
- nardial/conversation_state.py +295 -0
- nardial/dialog_logic.py +199 -0
- nardial/mini_dialogs.py +365 -0
- nardial/moves.py +212 -0
- nardial-0.1.0.dist-info/METADATA +69 -0
- nardial-0.1.0.dist-info/RECORD +15 -0
- nardial-0.1.0.dist-info/WHEEL +5 -0
- nardial-0.1.0.dist-info/licenses/LICENSE +661 -0
- nardial-0.1.0.dist-info/top_level.txt +1 -0
nardial/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .loader import load_dialogs
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Export dialogs.json to a readable HTML report for reviews/printing.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
- Default input: assets/dialogs/dialogs.json
|
|
6
|
+
- Default output: web/authoring/dialogs_report.html
|
|
7
|
+
|
|
8
|
+
python src/authoring/export_html.py
|
|
9
|
+
python src/authoring/export_html.py path/to/dialogs.json path/to/output.html
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import os
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from html import escape
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_dialogs(path: str):
|
|
21
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
if not isinstance(data, list):
|
|
24
|
+
raise ValueError("dialogs.json must be a list of dialogs")
|
|
25
|
+
# ensure unique ids
|
|
26
|
+
ids = [d.get("id") for d in data]
|
|
27
|
+
dups = {i for i in ids if ids.count(i) > 1}
|
|
28
|
+
if dups:
|
|
29
|
+
print(f"Warning: duplicate dialog ids found: {sorted(dups)}")
|
|
30
|
+
return data
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def render_moves(moves: list[dict]) -> str:
|
|
34
|
+
rows = []
|
|
35
|
+
for m in moves or []:
|
|
36
|
+
t = escape(str(m.get("type", "")))
|
|
37
|
+
branch = m.get("branch")
|
|
38
|
+
branch_s = f"<code class=branch>{escape(str(branch))}</code>" if branch not in (None, "") else ""
|
|
39
|
+
text = m.get("text")
|
|
40
|
+
audio = m.get("audio")
|
|
41
|
+
options = m.get("options")
|
|
42
|
+
next_map = m.get("next")
|
|
43
|
+
set_var = m.get("set_variable")
|
|
44
|
+
add_int = m.get("add_interest")
|
|
45
|
+
add_from_ans = m.get("add_interest_from_answer")
|
|
46
|
+
add_from_var = m.get("add_interest_from_variable")
|
|
47
|
+
|
|
48
|
+
details = []
|
|
49
|
+
if text:
|
|
50
|
+
details.append(f"<div class=mv-text>{escape(text)}</div>")
|
|
51
|
+
if audio:
|
|
52
|
+
details.append(f"<div class=mv-audio>audio: <code>{escape(audio)}</code></div>")
|
|
53
|
+
if options:
|
|
54
|
+
details.append("<div class=mv-options>options: " + ", ".join(f"<code>{escape(str(o))}</code>" for o in options) + "</div>")
|
|
55
|
+
if set_var:
|
|
56
|
+
details.append(f"<div class=mv-set>set_variable: <code>{escape(set_var)}</code></div>")
|
|
57
|
+
if add_int:
|
|
58
|
+
details.append(f"<div class=mv-add>add_interest: <code>{escape(add_int)}</code></div>")
|
|
59
|
+
if add_from_ans:
|
|
60
|
+
details.append("<div class=mv-add>add_interest_from_answer: <code>true</code></div>")
|
|
61
|
+
if add_from_var:
|
|
62
|
+
details.append("<div class=mv-add>add_interest_from_variable: <code>true</code></div>")
|
|
63
|
+
if next_map:
|
|
64
|
+
try:
|
|
65
|
+
next_json = json.dumps(next_map, ensure_ascii=False)
|
|
66
|
+
except Exception:
|
|
67
|
+
next_json = escape(str(next_map))
|
|
68
|
+
details.append(f"<div class=mv-next>next: <code>{escape(next_json)}</code></div>")
|
|
69
|
+
|
|
70
|
+
rows.append(
|
|
71
|
+
"""
|
|
72
|
+
<div class="move">
|
|
73
|
+
<div class="mv-head"><span class="mv-type">{t}</span> {branch}</div>
|
|
74
|
+
<div class="mv-body">{details}</div>
|
|
75
|
+
</div>
|
|
76
|
+
""".format(
|
|
77
|
+
t=t, branch=branch_s, details="".join(details)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return "\n".join(rows)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render_dialog(d: dict) -> str:
|
|
84
|
+
did = escape(str(d.get("id", "")))
|
|
85
|
+
dtype = escape(str(d.get("type", "")))
|
|
86
|
+
meta_bits = []
|
|
87
|
+
if dtype == "functional":
|
|
88
|
+
ft = d.get("functional_type")
|
|
89
|
+
if ft:
|
|
90
|
+
meta_bits.append(f"functional_type: <code>{escape(str(ft))}</code>")
|
|
91
|
+
if dtype == "narrative":
|
|
92
|
+
thr = d.get("thread")
|
|
93
|
+
pos = d.get("position")
|
|
94
|
+
if thr:
|
|
95
|
+
meta_bits.append(f"thread: <code>{escape(str(thr))}</code>")
|
|
96
|
+
if pos is not None:
|
|
97
|
+
meta_bits.append(f"position: <code>{escape(str(pos))}</code>")
|
|
98
|
+
if dtype == "chitchat":
|
|
99
|
+
theme = d.get("theme")
|
|
100
|
+
if theme:
|
|
101
|
+
meta_bits.append(f"theme: <code>{escape(str(theme))}</code>")
|
|
102
|
+
topics = d.get("topics") or []
|
|
103
|
+
if topics:
|
|
104
|
+
meta_bits.append("topics: " + ", ".join(f"<code>{escape(str(t))}</code>" for t in topics))
|
|
105
|
+
|
|
106
|
+
deps = d.get("dependencies") or []
|
|
107
|
+
vdeps = d.get("variable_dependencies") or []
|
|
108
|
+
if deps:
|
|
109
|
+
meta_bits.append("dependencies: " + ", ".join(f"<code>{escape(str(x))}</code>" for x in deps))
|
|
110
|
+
if vdeps:
|
|
111
|
+
# support strings or {variable, required}
|
|
112
|
+
def vd_to_str(v):
|
|
113
|
+
if isinstance(v, dict):
|
|
114
|
+
req = v.get("required")
|
|
115
|
+
return f"{v.get('variable')}" + (" (required)" if req else "")
|
|
116
|
+
return str(v)
|
|
117
|
+
meta_bits.append("variable_dependencies: " + ", ".join(f"<code>{escape(vd_to_str(x))}</code>" for x in vdeps))
|
|
118
|
+
|
|
119
|
+
moves_html = render_moves(d.get("moves") or [])
|
|
120
|
+
|
|
121
|
+
return f"""
|
|
122
|
+
<article class="dialog">
|
|
123
|
+
<h4><code>{did}</code> <span class="dtype">[{dtype}]</span></h4>
|
|
124
|
+
<div class="meta">{' • '.join(meta_bits) if meta_bits else ''}</div>
|
|
125
|
+
<div class="moves">{moves_html}</div>
|
|
126
|
+
</article>
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def build_html(dialogs: list[dict]) -> str:
|
|
131
|
+
# group
|
|
132
|
+
functionals = [d for d in dialogs if d.get("type") == "functional"]
|
|
133
|
+
narratives = [d for d in dialogs if d.get("type") == "narrative"]
|
|
134
|
+
chitchats = [d for d in dialogs if d.get("type") == "chitchat"]
|
|
135
|
+
|
|
136
|
+
narratives_by_thread = defaultdict(list)
|
|
137
|
+
for d in narratives:
|
|
138
|
+
narratives_by_thread[d.get("thread")].append(d)
|
|
139
|
+
for thr in narratives_by_thread:
|
|
140
|
+
narratives_by_thread[thr].sort(key=lambda x: (x.get("position") is None, x.get("position", 0)))
|
|
141
|
+
|
|
142
|
+
chitchats_by_theme = defaultdict(list)
|
|
143
|
+
for d in chitchats:
|
|
144
|
+
chitchats_by_theme[d.get("theme")].append(d)
|
|
145
|
+
for th in chitchats_by_theme:
|
|
146
|
+
chitchats_by_theme[th].sort(key=lambda x: x.get("id", ""))
|
|
147
|
+
|
|
148
|
+
def section(title: str, body: str) -> str:
|
|
149
|
+
return f"<section><h2>{escape(title)}</h2>{body}</section>"
|
|
150
|
+
|
|
151
|
+
functional_html = "".join(render_dialog(d) for d in sorted(functionals, key=lambda x: x.get("functional_type", "") + "_" + x.get("id", "")))
|
|
152
|
+
|
|
153
|
+
narrative_html = []
|
|
154
|
+
for thr in sorted(narratives_by_thread.keys(), key=lambda s: s or ""):
|
|
155
|
+
items = "".join(render_dialog(d) for d in narratives_by_thread[thr])
|
|
156
|
+
head = f"<h3>Thread: <code>{escape(str(thr))}</code></h3>" if thr else ""
|
|
157
|
+
narrative_html.append(f"<div class=thread>{head}{items}</div>")
|
|
158
|
+
|
|
159
|
+
chitchat_html = []
|
|
160
|
+
for th in sorted(chitchats_by_theme.keys(), key=lambda s: s or ""):
|
|
161
|
+
items = "".join(render_dialog(d) for d in chitchats_by_theme[th])
|
|
162
|
+
head = f"<h3>Theme: <code>{escape(str(th))}</code></h3>" if th else ""
|
|
163
|
+
chitchat_html.append(f"<div class=theme>{head}{items}</div>")
|
|
164
|
+
|
|
165
|
+
style = """
|
|
166
|
+
<style>
|
|
167
|
+
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;background:#0b0f14;color:#e7eefb;margin:0;padding:24px}
|
|
168
|
+
h1,h2,h3,h4{margin:0 0 8px 0}
|
|
169
|
+
h2{margin-top:24px;border-bottom:1px solid #233046;padding-bottom:6px}
|
|
170
|
+
section{margin-bottom:24px}
|
|
171
|
+
.dialog{border:1px solid #233046;border-radius:8px;margin:12px 0;padding:12px;background:#121821}
|
|
172
|
+
.dtype{color:#89a1bd;font-weight:normal}
|
|
173
|
+
.meta{color:#89a1bd;font-size:12px;margin-bottom:8px}
|
|
174
|
+
.moves{display:flex;flex-direction:column;gap:8px}
|
|
175
|
+
.move{border:1px solid #233046;border-radius:6px}
|
|
176
|
+
.mv-head{background:#0f1623;padding:6px 8px}
|
|
177
|
+
.mv-type{background:#4ea1ff;color:#001229;padding:1px 6px;border-radius:999px;font-size:12px}
|
|
178
|
+
.branch{color:#89a1bd}
|
|
179
|
+
.mv-body{padding:8px 10px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
|
|
180
|
+
code{background:#0f1623;border:1px solid #233046;border-radius:4px;padding:0 4px}
|
|
181
|
+
.thread,.theme{margin:8px 0}
|
|
182
|
+
</style>
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
html = f"""
|
|
186
|
+
<!doctype html>
|
|
187
|
+
<html lang=\"en\">\n<meta charset=\"utf-8\">\n<title>Dialogs Report</title>
|
|
188
|
+
{style}
|
|
189
|
+
<h1>Dialogs Report</h1>
|
|
190
|
+
{section('Functional', functional_html)}
|
|
191
|
+
{section('Narrative', ''.join(narrative_html))}
|
|
192
|
+
{section('Chitchat', ''.join(chitchat_html))}
|
|
193
|
+
"""
|
|
194
|
+
return html
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def main(argv: list[str]) -> int:
|
|
198
|
+
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
199
|
+
default_in = os.path.join(repo_root, "assets", "dialogs", "dialogs.json")
|
|
200
|
+
default_out = os.path.join(repo_root, "web", "authoring", "dialogs_report.html")
|
|
201
|
+
|
|
202
|
+
in_path = argv[1] if len(argv) > 1 else default_in
|
|
203
|
+
out_path = argv[2] if len(argv) > 2 else default_out
|
|
204
|
+
|
|
205
|
+
dialogs = load_dialogs(in_path)
|
|
206
|
+
html = build_html(dialogs)
|
|
207
|
+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
208
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
209
|
+
f.write(html)
|
|
210
|
+
print(f"Wrote HTML report: {out_path}")
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
if __name__ == "__main__":
|
|
215
|
+
raise SystemExit(main(sys.argv))
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from src.nardial.mini_dialogs import MiniDialog, NarrativeDialog, ChitchatDialog, FunctionalDialog, LLMDialog, DialogType
|
|
4
|
+
from src.nardial.moves import MOVE_SAY, MOVE_ASK_OPEN, MOVE_ASK_YESNO, MOVE_ASK_OPTIONS, MOVE_PLAY_AUDIO
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ALLOWED_MOVE_TYPES = {MOVE_SAY, MOVE_ASK_YESNO, MOVE_ASK_OPEN, MOVE_ASK_OPTIONS, MOVE_PLAY_AUDIO}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MoveFactory:
|
|
11
|
+
@staticmethod
|
|
12
|
+
def validate(move: Dict[str, Any], idx: int = 0) -> List[str]:
|
|
13
|
+
errs: List[str] = []
|
|
14
|
+
if not isinstance(move, dict):
|
|
15
|
+
return [f"moves[{idx}] must be an object"]
|
|
16
|
+
mt = move.get("type")
|
|
17
|
+
if mt not in ALLOWED_MOVE_TYPES:
|
|
18
|
+
errs.append(f"moves[{idx}].type must be one of {sorted(ALLOWED_MOVE_TYPES)}")
|
|
19
|
+
if mt in {"say"}:
|
|
20
|
+
if not isinstance(move.get("text"), str):
|
|
21
|
+
errs.append(f"moves[{idx}].text must be string for say")
|
|
22
|
+
if mt in {MOVE_ASK_YESNO, MOVE_ASK_OPEN, MOVE_ASK_OPTIONS}:
|
|
23
|
+
if not isinstance(move.get("text"), str):
|
|
24
|
+
errs.append(f"moves[{idx}].text must be string for {mt}")
|
|
25
|
+
if mt == "ask_options":
|
|
26
|
+
opts = move.get("options")
|
|
27
|
+
if not isinstance(opts, list) or not all(isinstance(o, str) for o in opts):
|
|
28
|
+
errs.append(f"moves[{idx}].options must be a list of strings for ask_options")
|
|
29
|
+
if "set_variable" in move and not isinstance(move.get("set_variable"), str):
|
|
30
|
+
errs.append(f"moves[{idx}].set_variable must be string if present")
|
|
31
|
+
return errs
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def normalize(move: Dict[str, Any]) -> Dict[str, Any]:
|
|
35
|
+
# Keep as-is; runtime expects dict moves. We could strip unknown keys later if needed.
|
|
36
|
+
return dict(move)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DialogFactory:
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _normalize_variable_dependencies(vdeps: Any) -> List[Dict[str, Any]]:
|
|
42
|
+
out: List[Dict[str, Any]] = []
|
|
43
|
+
if not vdeps:
|
|
44
|
+
return out
|
|
45
|
+
for item in vdeps:
|
|
46
|
+
if isinstance(item, str):
|
|
47
|
+
out.append({"variable": item, "required": True})
|
|
48
|
+
elif isinstance(item, dict) and item.get("variable"):
|
|
49
|
+
d = {"variable": str(item["variable"]), "required": bool(item.get("required", True))}
|
|
50
|
+
out.append(d)
|
|
51
|
+
return out
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def validate_doc(doc: Dict[str, Any]) -> List[str]:
|
|
55
|
+
errs: List[str] = []
|
|
56
|
+
t = doc.get("type")
|
|
57
|
+
did = doc.get("id")
|
|
58
|
+
if not isinstance(did, str) or not did:
|
|
59
|
+
errs.append("id must be non-empty string")
|
|
60
|
+
if t not in {"functional", "narrative", "chitchat"}:
|
|
61
|
+
errs.append("type must be 'functional' | 'narrative' | 'chitchat'")
|
|
62
|
+
# shared
|
|
63
|
+
deps = doc.get("dependencies")
|
|
64
|
+
if deps is not None and (not isinstance(deps, list) or not all(isinstance(x, str) for x in deps)):
|
|
65
|
+
errs.append("dependencies must be a list of strings")
|
|
66
|
+
# variable deps: allow list[str|obj]
|
|
67
|
+
vdeps = doc.get("variable_dependencies")
|
|
68
|
+
if vdeps is not None:
|
|
69
|
+
if not isinstance(vdeps, list):
|
|
70
|
+
errs.append("variable_dependencies must be a list")
|
|
71
|
+
else:
|
|
72
|
+
for idx, vd in enumerate(vdeps):
|
|
73
|
+
if isinstance(vd, str):
|
|
74
|
+
continue
|
|
75
|
+
if not isinstance(vd, dict) or "variable" not in vd:
|
|
76
|
+
errs.append(f"variable_dependencies[{idx}] must be string or object with 'variable'")
|
|
77
|
+
# type-specific
|
|
78
|
+
if t == "functional":
|
|
79
|
+
if not isinstance(doc.get("functional_type"), str):
|
|
80
|
+
errs.append("functional_type must be string for functional dialogs")
|
|
81
|
+
elif t == "narrative":
|
|
82
|
+
if not isinstance(doc.get("thread"), str):
|
|
83
|
+
errs.append("thread must be string for narrative dialogs")
|
|
84
|
+
try:
|
|
85
|
+
int(doc.get("position"))
|
|
86
|
+
except Exception:
|
|
87
|
+
errs.append("position must be integer for narrative dialogs")
|
|
88
|
+
elif t == "chitchat":
|
|
89
|
+
if not isinstance(doc.get("theme"), str):
|
|
90
|
+
errs.append("theme must be string for chitchat dialogs")
|
|
91
|
+
topics = doc.get("topics")
|
|
92
|
+
if topics is not None and (not isinstance(topics, list) or not all(isinstance(x, str) for x in topics)):
|
|
93
|
+
errs.append("topics must be a list of strings for chitchat dialogs")
|
|
94
|
+
|
|
95
|
+
moves = doc.get("moves")
|
|
96
|
+
if not isinstance(moves, list):
|
|
97
|
+
errs.append("moves must be a list")
|
|
98
|
+
else:
|
|
99
|
+
for i, mv in enumerate(moves):
|
|
100
|
+
errs.extend(MoveFactory.validate(mv, idx=i))
|
|
101
|
+
return errs
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def from_json(doc: Dict[str, Any]) -> MiniDialog:
|
|
105
|
+
errors = DialogFactory.validate_doc(doc)
|
|
106
|
+
if errors:
|
|
107
|
+
raise ValueError("; ".join(errors))
|
|
108
|
+
|
|
109
|
+
dtype = doc.get("type")
|
|
110
|
+
did = doc.get("id")
|
|
111
|
+
deps = list(doc.get("dependencies") or [])
|
|
112
|
+
vdeps = DialogFactory._normalize_variable_dependencies(doc.get("variable_dependencies"))
|
|
113
|
+
moves = [MoveFactory.normalize(m) for m in (doc.get("moves") or [])]
|
|
114
|
+
|
|
115
|
+
if dtype == DialogType.NARRATIVE.value:
|
|
116
|
+
return NarrativeDialog(
|
|
117
|
+
dialog_id=did,
|
|
118
|
+
moves=moves,
|
|
119
|
+
thread=doc["thread"],
|
|
120
|
+
position=int(doc["position"]),
|
|
121
|
+
dependencies=deps,
|
|
122
|
+
variable_dependencies=vdeps,
|
|
123
|
+
)
|
|
124
|
+
if dtype == DialogType.CHITCHAT.value:
|
|
125
|
+
return ChitchatDialog(
|
|
126
|
+
dialog_id=did,
|
|
127
|
+
moves=moves,
|
|
128
|
+
theme=doc.get("theme") or "",
|
|
129
|
+
topics=list(doc.get("topics") or []),
|
|
130
|
+
dependencies=deps,
|
|
131
|
+
variable_dependencies=vdeps,
|
|
132
|
+
)
|
|
133
|
+
if dtype == DialogType.FUNCTIONAL.value:
|
|
134
|
+
return FunctionalDialog(
|
|
135
|
+
dialog_id=did,
|
|
136
|
+
moves=moves,
|
|
137
|
+
type=doc["functional_type"],
|
|
138
|
+
dependencies=deps,
|
|
139
|
+
)
|
|
140
|
+
if dtype == DialogType.LLM_BASED.value:
|
|
141
|
+
return LLMDialog(
|
|
142
|
+
dialog_id=did,
|
|
143
|
+
moves=moves,
|
|
144
|
+
prompt=doc["prompt"],
|
|
145
|
+
max_turns=doc["max_turns"],
|
|
146
|
+
dependencies=deps,
|
|
147
|
+
variable_dependencies=vdeps,
|
|
148
|
+
)
|
|
149
|
+
return MiniDialog(did, moves, deps, vdeps)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def to_json(d: MiniDialog) -> Dict[str, Any]:
|
|
153
|
+
base: Dict[str, Any] = {
|
|
154
|
+
"id": getattr(d, "dialog_id", None),
|
|
155
|
+
"dependencies": list(getattr(d, "dependencies", []) or []),
|
|
156
|
+
"variable_dependencies": list(getattr(d, "variable_dependencies", []) or []),
|
|
157
|
+
"moves": list(getattr(d, "moves", []) or []),
|
|
158
|
+
}
|
|
159
|
+
if isinstance(d, NarrativeDialog):
|
|
160
|
+
base.update({
|
|
161
|
+
"type": "narrative",
|
|
162
|
+
"thread": getattr(d, "thread", ""),
|
|
163
|
+
"position": int(getattr(d, "position", 0)),
|
|
164
|
+
})
|
|
165
|
+
elif isinstance(d, ChitchatDialog):
|
|
166
|
+
base.update({
|
|
167
|
+
"type": "chitchat",
|
|
168
|
+
"theme": getattr(d, "theme", ""),
|
|
169
|
+
"topics": list(getattr(d, "topics", []) or []),
|
|
170
|
+
})
|
|
171
|
+
elif isinstance(d, FunctionalDialog):
|
|
172
|
+
base.update({
|
|
173
|
+
"type": "functional",
|
|
174
|
+
"functional_type": getattr(d, "type", ""),
|
|
175
|
+
})
|
|
176
|
+
else:
|
|
177
|
+
base.update({"type": "unknown"})
|
|
178
|
+
return base
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict, List, Tuple
|
|
4
|
+
|
|
5
|
+
from src.nardial.authoring.factory import DialogFactory
|
|
6
|
+
from src.nardial.mini_dialogs import MiniDialog
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _load_json_file(path: str) -> List[Dict[str, Any]]:
|
|
10
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
11
|
+
data = json.load(f)
|
|
12
|
+
if isinstance(data, list):
|
|
13
|
+
return data
|
|
14
|
+
if isinstance(data, dict):
|
|
15
|
+
return [data]
|
|
16
|
+
raise ValueError(f"Unsupported JSON root in {path}: {type(data)}")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_dialogs(path_or_dir: str) -> Tuple[List[MiniDialog], List[str]]:
|
|
20
|
+
"""Load dialogs from a JSON file or all .json files in a directory.
|
|
21
|
+
|
|
22
|
+
Returns (dialogs, errors).
|
|
23
|
+
"""
|
|
24
|
+
dialogs: List[MiniDialog] = []
|
|
25
|
+
errors: List[str] = []
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
if os.path.isdir(path_or_dir):
|
|
29
|
+
for fn in os.listdir(path_or_dir):
|
|
30
|
+
if not fn.lower().endswith(".json"):
|
|
31
|
+
continue
|
|
32
|
+
p = os.path.join(path_or_dir, fn)
|
|
33
|
+
try:
|
|
34
|
+
for doc in _load_json_file(p):
|
|
35
|
+
dialogs.append(DialogFactory.from_json(doc))
|
|
36
|
+
except Exception as e:
|
|
37
|
+
errors.append(f"{p}: {e}")
|
|
38
|
+
else:
|
|
39
|
+
for doc in _load_json_file(path_or_dir):
|
|
40
|
+
try:
|
|
41
|
+
dialogs.append(DialogFactory.from_json(doc))
|
|
42
|
+
except Exception as e:
|
|
43
|
+
errors.append(f"{path_or_dir}: {e}")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
errors.append(str(e))
|
|
46
|
+
|
|
47
|
+
return dialogs, errors
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dialog_to_doc(d: MiniDialog) -> Dict[str, Any]:
|
|
51
|
+
"""Serialize a dialog object back to a JSON-ready dict (round-trip)."""
|
|
52
|
+
return DialogFactory.to_json(d)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def save_dialogs(path: str, dialogs: List[MiniDialog]) -> None:
|
|
56
|
+
"""Save a list of dialog objects to a single JSON file (array root)."""
|
|
57
|
+
docs = [dialog_to_doc(d) for d in dialogs]
|
|
58
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
59
|
+
json.dump(docs, f, indent=2, ensure_ascii=False)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save_dialogs_to_dir(directory: str, dialogs: List[MiniDialog]) -> None:
|
|
63
|
+
"""Save each dialog to its own JSON file inside directory."""
|
|
64
|
+
os.makedirs(directory, exist_ok=True)
|
|
65
|
+
for d in dialogs:
|
|
66
|
+
did = getattr(d, "dialog_id", None) or "untitled"
|
|
67
|
+
safe_id = "".join(c for c in did if c.isalnum() or c in ("_", "-"))
|
|
68
|
+
path = os.path.join(directory, f"{safe_id}.json")
|
|
69
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
70
|
+
json.dump(dialog_to_doc(d), f, indent=2, ensure_ascii=False)
|