agent-sin 0.1.12 → 0.1.15
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.
- package/CHANGELOG.md +66 -0
- package/README.md +2 -1
- package/builtin-skills/_shared/_todo_lib.py +290 -0
- package/builtin-skills/even-g2-setup/main.ts +896 -0
- package/builtin-skills/even-g2-setup/skill.yaml +133 -0
- package/builtin-skills/memo-delete/main.py +28 -107
- package/builtin-skills/memo-delete/skill.yaml +10 -21
- package/builtin-skills/memo-index/main.py +96 -64
- package/builtin-skills/memo-index/skill.yaml +4 -10
- package/builtin-skills/memo-list/main.py +126 -72
- package/builtin-skills/memo-list/skill.yaml +8 -14
- package/builtin-skills/memo-save/main.py +191 -25
- package/builtin-skills/memo-save/skill.yaml +29 -5
- package/builtin-skills/memo-search/main.py +38 -18
- package/builtin-skills/memo-vector-search/main.py +11 -6
- package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
- package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
- package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
- package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
- package/builtin-skills/schedule-add/main.py +26 -0
- package/builtin-skills/service-restart/main.ts +249 -0
- package/builtin-skills/service-restart/skill.yaml +49 -0
- package/builtin-skills/todo-add/main.py +3 -1
- package/builtin-skills/todo-delete/main.py +3 -1
- package/builtin-skills/todo-done/main.py +3 -1
- package/builtin-skills/todo-list/main.py +4 -1
- package/builtin-skills/todo-tick/main.py +3 -1
- package/builtin-skills/topic-knowledge-read/main.py +118 -0
- package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +82 -1
- package/dist/builder/build-flow.d.ts +33 -4
- package/dist/builder/build-flow.js +251 -89
- package/dist/builder/builder-session.d.ts +1 -1
- package/dist/builder/builder-session.js +112 -7
- package/dist/builder/conversation-router.d.ts +4 -2
- package/dist/builder/conversation-router.js +19 -2
- package/dist/cli/index.js +323 -20
- package/dist/core/ai-provider.d.ts +1 -0
- package/dist/core/ai-provider.js +8 -3
- package/dist/core/chat-engine.d.ts +9 -3
- package/dist/core/chat-engine.js +1263 -146
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +82 -0
- package/dist/core/daily-memory-promotion.d.ts +7 -0
- package/dist/core/daily-memory-promotion.js +568 -14
- package/dist/core/image-attachments.d.ts +31 -0
- package/dist/core/image-attachments.js +237 -0
- package/dist/core/logger.d.ts +2 -1
- package/dist/core/logger.js +77 -1
- package/dist/core/memo-migration.d.ts +3 -0
- package/dist/core/memo-migration.js +422 -0
- package/dist/core/native-modules.d.ts +24 -0
- package/dist/core/native-modules.js +99 -0
- package/dist/core/notifier.d.ts +8 -3
- package/dist/core/notifier.js +191 -17
- package/dist/core/obsidian-vault.d.ts +19 -0
- package/dist/core/obsidian-vault.js +477 -0
- package/dist/core/operating-model.d.ts +2 -0
- package/dist/core/operating-model.js +15 -0
- package/dist/core/output-writer.d.ts +3 -2
- package/dist/core/output-writer.js +108 -7
- package/dist/core/profile-memory.js +22 -1
- package/dist/core/runtime.d.ts +2 -0
- package/dist/core/runtime.js +9 -1
- package/dist/core/secrets.d.ts +4 -0
- package/dist/core/secrets.js +34 -0
- package/dist/core/skill-history.d.ts +44 -0
- package/dist/core/skill-history.js +329 -0
- package/dist/core/skill-registry.d.ts +5 -0
- package/dist/core/skill-registry.js +11 -0
- package/dist/discord/bot.d.ts +1 -0
- package/dist/discord/bot.js +181 -10
- package/dist/even-g2/gateway.d.ts +15 -0
- package/dist/even-g2/gateway.js +868 -0
- package/dist/runtimes/codex-app-server.d.ts +5 -1
- package/dist/runtimes/codex-app-server.js +147 -8
- package/dist/runtimes/python-runner.js +82 -0
- package/dist/runtimes/typescript-runner.js +13 -1
- package/dist/skills-sdk/types.d.ts +19 -4
- package/dist/telegram/bot.d.ts +1 -0
- package/dist/telegram/bot.js +115 -7
- package/package.json +3 -1
- package/templates/even-g2-agent/README.md +83 -0
- package/templates/even-g2-agent/app.json +20 -0
- package/templates/even-g2-agent/index.html +31 -0
- package/templates/even-g2-agent/package-lock.json +1836 -0
- package/templates/even-g2-agent/package.json +22 -0
- package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
- package/templates/even-g2-agent/src/embedded-config.ts +4 -0
- package/templates/even-g2-agent/src/main.ts +539 -0
- package/templates/even-g2-agent/src/style.css +70 -0
- package/templates/even-g2-agent/tsconfig.json +11 -0
- package/templates/skill-python/main.py +20 -2
- package/templates/skill-python/skill.yaml +9 -0
- package/templates/skill-typescript/main.ts +40 -5
- package/templates/skill-typescript/skill.yaml +9 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""Builtin: nightly-topic-knowledge
|
|
2
|
+
|
|
3
|
+
Reads the previous day's daily memory, conversation log and notes, asks a local
|
|
4
|
+
Ollama model to distill them into per-topic JSON, and merges the result into
|
|
5
|
+
memory/topic-knowledge/. Designed to run once a day via cron.
|
|
6
|
+
|
|
7
|
+
This skill writes its own files (multiple JSONs per run) so it bypasses the
|
|
8
|
+
runtime's declarative outputs system. memory.md is never touched.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import urllib.error
|
|
17
|
+
import urllib.request
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_shared"))
|
|
22
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
23
|
+
from i18n import localizer # noqa: E402
|
|
24
|
+
from _topics_lib import ( # noqa: E402
|
|
25
|
+
DEFAULT_MAX_CHARS,
|
|
26
|
+
DEFAULT_TIMEZONE,
|
|
27
|
+
build_index,
|
|
28
|
+
build_prompt,
|
|
29
|
+
bundle_sources,
|
|
30
|
+
index_path,
|
|
31
|
+
list_existing_topic_ids,
|
|
32
|
+
load_json,
|
|
33
|
+
merge_topic,
|
|
34
|
+
normalize_topics,
|
|
35
|
+
parse_llm_response,
|
|
36
|
+
read_conversation_log,
|
|
37
|
+
read_daily_memory,
|
|
38
|
+
read_notes,
|
|
39
|
+
resolve_target_date,
|
|
40
|
+
run_path,
|
|
41
|
+
topic_path,
|
|
42
|
+
write_json,
|
|
43
|
+
)
|
|
44
|
+
from _feedback_lib import ( # noqa: E402
|
|
45
|
+
build_feedback_prompt,
|
|
46
|
+
collect_skill_summary,
|
|
47
|
+
format_notification,
|
|
48
|
+
format_proposal_for_history,
|
|
49
|
+
parse_feedback_response,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
OLLAMA_PREFIX = "ollama:"
|
|
54
|
+
DEFAULT_MODEL_LABEL = "chat"
|
|
55
|
+
FAKE_ENV = "NIGHTLY_TOPIC_KNOWLEDGE_FAKE_JSON"
|
|
56
|
+
FAKE_FEEDBACK_ENV = "NIGHTLY_TOPIC_KNOWLEDGE_FAKE_FEEDBACK_JSON"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def run(ctx, input):
|
|
60
|
+
loc = localizer(input)
|
|
61
|
+
args = input.get("args", {}) or {}
|
|
62
|
+
sources = input.get("sources", {}) or {}
|
|
63
|
+
|
|
64
|
+
workspace = sources.get("workspace")
|
|
65
|
+
memory_dir = sources.get("memory_dir")
|
|
66
|
+
notes_dir = sources.get("notes_dir")
|
|
67
|
+
logs_dir = sources.get("logs_dir")
|
|
68
|
+
if not workspace or not memory_dir or not notes_dir or not logs_dir:
|
|
69
|
+
return _err(loc.t("Workspace unavailable", "ワークスペース不明"),
|
|
70
|
+
loc.t("workspace / memory_dir / notes_dir / logs_dir not provided.",
|
|
71
|
+
"workspace / memory_dir / notes_dir / logs_dir が取得できません"))
|
|
72
|
+
|
|
73
|
+
tz_name = str(args.get("timezone") or DEFAULT_TIMEZONE).strip() or DEFAULT_TIMEZONE
|
|
74
|
+
model_arg = str(args.get("model") or "").strip()
|
|
75
|
+
max_chars = int(args.get("max_chars") or DEFAULT_MAX_CHARS)
|
|
76
|
+
dry_run = bool(args.get("dry_run"))
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
date_str = resolve_target_date(args.get("date"), tz_name)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return _err(loc.t("Invalid date", "日付不正"), str(e))
|
|
82
|
+
|
|
83
|
+
daily_path, daily_text = read_daily_memory(memory_dir, date_str)
|
|
84
|
+
convo_path, convo_text = read_conversation_log(logs_dir, date_str)
|
|
85
|
+
notes_path, notes_text = read_notes(notes_dir, date_str)
|
|
86
|
+
|
|
87
|
+
sources_used = [p for p in (daily_path, convo_path, notes_path) if p]
|
|
88
|
+
bundle, bundle_chars = bundle_sources(
|
|
89
|
+
[
|
|
90
|
+
("daily-memory", daily_text),
|
|
91
|
+
("conversations", convo_text),
|
|
92
|
+
("notes", notes_text),
|
|
93
|
+
],
|
|
94
|
+
max_chars,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not bundle.strip():
|
|
98
|
+
ctx.log.info(f"nightly-topic-knowledge: nothing to read for {date_str}")
|
|
99
|
+
return {
|
|
100
|
+
"status": "skipped",
|
|
101
|
+
"title": loc.t("No sources", "対象なし"),
|
|
102
|
+
"summary": loc.t(
|
|
103
|
+
f"No daily memory / conversation log / notes found for {date_str}.",
|
|
104
|
+
f"{date_str} の日次メモ・会話ログ・ノートが見つかりませんでした",
|
|
105
|
+
),
|
|
106
|
+
"outputs": {},
|
|
107
|
+
"data": {"date": date_str, "sources_used": sources_used},
|
|
108
|
+
"suggestions": [],
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
prompt = build_prompt(date_str, bundle, list_existing_topic_ids(memory_dir))
|
|
112
|
+
|
|
113
|
+
model_label = model_arg or DEFAULT_MODEL_LABEL
|
|
114
|
+
try:
|
|
115
|
+
raw_text, model_label = await _call_model(ctx, model_arg, prompt)
|
|
116
|
+
except Exception as e:
|
|
117
|
+
ctx.log.error(f"nightly-topic-knowledge: model call failed: {e}")
|
|
118
|
+
return _err(
|
|
119
|
+
loc.t("Model call failed", "モデル呼び出し失敗"),
|
|
120
|
+
loc.t(
|
|
121
|
+
f"Failed to call {model_label}: {e}",
|
|
122
|
+
f"{model_label} の呼び出しに失敗しました: {e}",
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
parsed = parse_llm_response(raw_text)
|
|
128
|
+
topic_updates = normalize_topics(parsed)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
ctx.log.error(f"nightly-topic-knowledge: invalid response: {e}")
|
|
131
|
+
return _err(
|
|
132
|
+
loc.t("Invalid response", "応答不正"),
|
|
133
|
+
loc.t(f"Could not parse model response as JSON: {e}", f"モデル応答をJSONとして解釈できません: {e}"),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not topic_updates:
|
|
137
|
+
ctx.log.info(f"nightly-topic-knowledge: model returned no topics for {date_str}")
|
|
138
|
+
return {
|
|
139
|
+
"status": "skipped",
|
|
140
|
+
"title": loc.t("No topics", "トピックなし"),
|
|
141
|
+
"summary": loc.t(
|
|
142
|
+
f"Model produced no usable topics for {date_str}.",
|
|
143
|
+
f"{date_str} のソースから有用なトピックは抽出されませんでした",
|
|
144
|
+
),
|
|
145
|
+
"outputs": {},
|
|
146
|
+
"data": {
|
|
147
|
+
"date": date_str,
|
|
148
|
+
"sources_used": sources_used,
|
|
149
|
+
"input_chars": bundle_chars,
|
|
150
|
+
"model": model_label,
|
|
151
|
+
"raw_response_chars": len(raw_text),
|
|
152
|
+
},
|
|
153
|
+
"suggestions": [],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
now_iso = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
157
|
+
merged_topics: list[dict[str, Any]] = []
|
|
158
|
+
for update in topic_updates:
|
|
159
|
+
tid = update["topic_id"]
|
|
160
|
+
existing = load_json(topic_path(memory_dir, tid))
|
|
161
|
+
merged = merge_topic(existing, update, date_str, now_iso)
|
|
162
|
+
merged_topics.append(merged)
|
|
163
|
+
|
|
164
|
+
index = build_index(load_json(index_path(memory_dir)), merged_topics, now_iso)
|
|
165
|
+
|
|
166
|
+
run_record = {
|
|
167
|
+
"date": date_str,
|
|
168
|
+
"generated_at": now_iso,
|
|
169
|
+
"model": model_label,
|
|
170
|
+
"timezone": tz_name,
|
|
171
|
+
"sources_used": sources_used,
|
|
172
|
+
"input_chars": bundle_chars,
|
|
173
|
+
"raw_response_chars": len(raw_text),
|
|
174
|
+
"topics_updated": [topic["topic_id"] for topic in merged_topics],
|
|
175
|
+
"dry_run": dry_run,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if not dry_run:
|
|
179
|
+
for topic in merged_topics:
|
|
180
|
+
write_json(topic_path(memory_dir, topic["topic_id"]), topic)
|
|
181
|
+
write_json(index_path(memory_dir), index)
|
|
182
|
+
write_json(run_path(memory_dir, date_str), run_record)
|
|
183
|
+
|
|
184
|
+
feedback_result = await _run_skill_feedback(
|
|
185
|
+
ctx,
|
|
186
|
+
date_str=date_str,
|
|
187
|
+
bundle=bundle,
|
|
188
|
+
sources=sources,
|
|
189
|
+
dry_run=dry_run,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
ctx.log.info(
|
|
193
|
+
f"nightly-topic-knowledge: {date_str} model={model_label} topics={len(merged_topics)} "
|
|
194
|
+
f"feedback={feedback_result['count']} dry_run={dry_run}"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"status": "ok",
|
|
199
|
+
"title": loc.t("Distilled", "整理しました"),
|
|
200
|
+
"summary": loc.t(
|
|
201
|
+
f"Distilled {date_str}: {len(merged_topics)} topic(s){' (dry-run)' if dry_run else ''}",
|
|
202
|
+
f"{date_str} を整理しました: {len(merged_topics)}トピック{'(dry-run)' if dry_run else ''}",
|
|
203
|
+
),
|
|
204
|
+
"outputs": {},
|
|
205
|
+
"data": {
|
|
206
|
+
"date": date_str,
|
|
207
|
+
"model": model_label,
|
|
208
|
+
"sources_used": sources_used,
|
|
209
|
+
"input_chars": bundle_chars,
|
|
210
|
+
"topics_updated": run_record["topics_updated"],
|
|
211
|
+
"dry_run": dry_run,
|
|
212
|
+
"run_path": run_path(memory_dir, date_str) if not dry_run else None,
|
|
213
|
+
"topics": merged_topics if dry_run else None,
|
|
214
|
+
"skill_feedback": feedback_result,
|
|
215
|
+
},
|
|
216
|
+
"suggestions": [],
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def _call_model(ctx, model_arg: str, prompt: str) -> tuple[str, str]:
|
|
221
|
+
"""Returns (raw_text, model_label).
|
|
222
|
+
|
|
223
|
+
No model_arg → route through the configured chat model (works for any
|
|
224
|
+
provider the user has set up: API / CLI / Ollama). Explicit
|
|
225
|
+
"ollama:<name>" → talk to a local Ollama directly so users can pin a cheap
|
|
226
|
+
local model for the nightly run without changing models.yaml. Any other
|
|
227
|
+
explicit value is rejected — those should be wired in models.yaml and
|
|
228
|
+
selected via the chat role instead.
|
|
229
|
+
"""
|
|
230
|
+
# Validate input before the test/offline hook so an invalid override is
|
|
231
|
+
# rejected even when the fake env is set (otherwise our own tests for
|
|
232
|
+
# invalid input could pass for the wrong reason).
|
|
233
|
+
if model_arg and not model_arg.startswith(OLLAMA_PREFIX):
|
|
234
|
+
raise RuntimeError(
|
|
235
|
+
f'Direct mode supports only Ollama models (got "{model_arg}"). '
|
|
236
|
+
f'Use "ollama:<name>", or omit `model` to use the configured chat model.'
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
fake = os.environ.get(FAKE_ENV)
|
|
240
|
+
if fake:
|
|
241
|
+
return fake, (model_arg or DEFAULT_MODEL_LABEL)
|
|
242
|
+
|
|
243
|
+
if not model_arg:
|
|
244
|
+
result = await ctx.ai.run("distill", prompt)
|
|
245
|
+
if not isinstance(result, dict) or result.get("status") != "ok":
|
|
246
|
+
reason = (result or {}).get("reason") if isinstance(result, dict) else None
|
|
247
|
+
raise RuntimeError(reason or "chat model returned no result")
|
|
248
|
+
text = result.get("text") or ""
|
|
249
|
+
provider = result.get("provider") or "ai"
|
|
250
|
+
model_id = result.get("model_id") or "chat"
|
|
251
|
+
return text, f"{provider}:{model_id}"
|
|
252
|
+
|
|
253
|
+
text = _call_ollama(model_arg, prompt)
|
|
254
|
+
return text, model_arg
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _call_ollama(model_id: str, prompt: str) -> str:
|
|
258
|
+
ollama_model = model_id[len(OLLAMA_PREFIX):]
|
|
259
|
+
if not ollama_model:
|
|
260
|
+
raise RuntimeError("Ollama model name is empty after the prefix.")
|
|
261
|
+
host = os.environ.get("OLLAMA_HOST") or "http://localhost:11434"
|
|
262
|
+
url = host.rstrip("/") + "/api/chat"
|
|
263
|
+
body = json.dumps(
|
|
264
|
+
{
|
|
265
|
+
"model": ollama_model,
|
|
266
|
+
"messages": [
|
|
267
|
+
{
|
|
268
|
+
"role": "system",
|
|
269
|
+
"content": "You distill conversation logs into per-topic JSON.",
|
|
270
|
+
},
|
|
271
|
+
{"role": "user", "content": prompt},
|
|
272
|
+
],
|
|
273
|
+
"stream": False,
|
|
274
|
+
"format": "json",
|
|
275
|
+
"options": {"temperature": 0.2},
|
|
276
|
+
},
|
|
277
|
+
ensure_ascii=False,
|
|
278
|
+
).encode("utf-8")
|
|
279
|
+
request = urllib.request.Request(
|
|
280
|
+
url,
|
|
281
|
+
data=body,
|
|
282
|
+
headers={"Content-Type": "application/json"},
|
|
283
|
+
method="POST",
|
|
284
|
+
)
|
|
285
|
+
try:
|
|
286
|
+
with urllib.request.urlopen(request, timeout=300) as response:
|
|
287
|
+
payload = response.read().decode("utf-8")
|
|
288
|
+
except urllib.error.HTTPError as e:
|
|
289
|
+
detail = e.read().decode("utf-8", errors="replace") if hasattr(e, "read") else str(e)
|
|
290
|
+
raise RuntimeError(f"Ollama HTTP {e.code}: {detail[:400]}")
|
|
291
|
+
except urllib.error.URLError as e:
|
|
292
|
+
raise RuntimeError(f"Ollama unreachable at {host}: {e.reason}")
|
|
293
|
+
parsed = json.loads(payload)
|
|
294
|
+
message = parsed.get("message") or {}
|
|
295
|
+
content = message.get("content")
|
|
296
|
+
if not isinstance(content, str):
|
|
297
|
+
raise RuntimeError("Ollama response had no message.content")
|
|
298
|
+
return content
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def _run_skill_feedback(
|
|
302
|
+
ctx,
|
|
303
|
+
*,
|
|
304
|
+
date_str: str,
|
|
305
|
+
bundle: str,
|
|
306
|
+
sources: dict[str, Any],
|
|
307
|
+
dry_run: bool,
|
|
308
|
+
) -> dict[str, Any]:
|
|
309
|
+
"""Second AI step: derive skill improvement / new-skill proposals.
|
|
310
|
+
|
|
311
|
+
Failures here must not break the nightly run — feedback is best-effort
|
|
312
|
+
on top of the topic-knowledge distillation. Any error is logged and
|
|
313
|
+
surfaced in the returned summary instead of raising.
|
|
314
|
+
"""
|
|
315
|
+
skills_dir = sources.get("skills_dir")
|
|
316
|
+
builtin_skills_dir = sources.get("builtin_skills_dir")
|
|
317
|
+
catalog = collect_skill_summary(skills_dir, builtin_skills_dir)
|
|
318
|
+
prompt = build_feedback_prompt(date_str, bundle, catalog)
|
|
319
|
+
|
|
320
|
+
raw_text = ""
|
|
321
|
+
try:
|
|
322
|
+
fake = os.environ.get(FAKE_FEEDBACK_ENV)
|
|
323
|
+
if fake:
|
|
324
|
+
raw_text = fake
|
|
325
|
+
else:
|
|
326
|
+
response = await ctx.ai.run("skill_feedback", prompt)
|
|
327
|
+
if not isinstance(response, dict) or response.get("status") != "ok":
|
|
328
|
+
reason = (response or {}).get("reason") if isinstance(response, dict) else None
|
|
329
|
+
ctx.log.warn(f"nightly-topic-knowledge: skill_feedback skipped: {reason or 'no result'}")
|
|
330
|
+
return {"count": 0, "notified": False, "skipped_reason": reason or "no result"}
|
|
331
|
+
raw_text = response.get("text") or ""
|
|
332
|
+
except Exception as e:
|
|
333
|
+
ctx.log.warn(f"nightly-topic-knowledge: skill_feedback failed: {e}")
|
|
334
|
+
return {"count": 0, "notified": False, "skipped_reason": str(e)}
|
|
335
|
+
|
|
336
|
+
proposals = parse_feedback_response(raw_text)
|
|
337
|
+
if not proposals:
|
|
338
|
+
ctx.log.info(f"nightly-topic-knowledge: skill_feedback produced no proposals for {date_str}")
|
|
339
|
+
return {"count": 0, "notified": False, "skipped_reason": "empty"}
|
|
340
|
+
|
|
341
|
+
saved_meta: list[dict[str, Any]] = []
|
|
342
|
+
now_iso = datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
343
|
+
for index, proposal in enumerate(proposals, start=1):
|
|
344
|
+
entry_id = f"{date_str}#{index}"
|
|
345
|
+
content = format_proposal_for_history(date_str, index, proposal)
|
|
346
|
+
meta = {
|
|
347
|
+
"id": entry_id,
|
|
348
|
+
"date": date_str,
|
|
349
|
+
"index": index,
|
|
350
|
+
"type": proposal["type"],
|
|
351
|
+
"target_kind": proposal["target_kind"],
|
|
352
|
+
"target_skill": proposal.get("target_skill") or "",
|
|
353
|
+
"target_change": proposal.get("target_change") or "other",
|
|
354
|
+
"status": "pending",
|
|
355
|
+
"generated_at": now_iso,
|
|
356
|
+
}
|
|
357
|
+
if not dry_run:
|
|
358
|
+
try:
|
|
359
|
+
await ctx.history.append({"meta": meta, "content": content, "replace": True})
|
|
360
|
+
except Exception as e:
|
|
361
|
+
ctx.log.warn(f"nightly-topic-knowledge: history.append failed for {entry_id}: {e}")
|
|
362
|
+
continue
|
|
363
|
+
saved_meta.append(meta)
|
|
364
|
+
|
|
365
|
+
notified = False
|
|
366
|
+
notify_detail: str | None = None
|
|
367
|
+
if saved_meta and not dry_run:
|
|
368
|
+
message = format_notification(date_str, proposals[: len(saved_meta)])
|
|
369
|
+
if message:
|
|
370
|
+
try:
|
|
371
|
+
outcome = await ctx.notify({
|
|
372
|
+
"title": f"スキル振り返り {date_str}",
|
|
373
|
+
"body": message,
|
|
374
|
+
})
|
|
375
|
+
notified = bool(outcome.get("ok"))
|
|
376
|
+
notify_detail = outcome.get("detail")
|
|
377
|
+
if not notified:
|
|
378
|
+
ctx.log.warn(f"nightly-topic-knowledge: notify failed: {notify_detail}")
|
|
379
|
+
except Exception as e:
|
|
380
|
+
ctx.log.warn(f"nightly-topic-knowledge: notify error: {e}")
|
|
381
|
+
notify_detail = str(e)
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
"count": len(saved_meta),
|
|
385
|
+
"notified": notified,
|
|
386
|
+
"notify_detail": notify_detail,
|
|
387
|
+
"proposals": proposals if dry_run else None,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _err(title: str, summary: str) -> dict[str, Any]:
|
|
392
|
+
return {
|
|
393
|
+
"status": "error",
|
|
394
|
+
"title": title,
|
|
395
|
+
"summary": summary,
|
|
396
|
+
"outputs": {},
|
|
397
|
+
"data": {},
|
|
398
|
+
"suggestions": [],
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# Re-export for tests
|
|
403
|
+
__all__ = ["run"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Builtin: nightly-topic-knowledge
|
|
2
|
+
# Reads the previous day's conversations, daily memory and notes after midnight,
|
|
3
|
+
# distills them into per-topic lightweight knowledge files, and updates an index.
|
|
4
|
+
# Designed to be invoked once a day via cron (e.g. "10 0 * * *").
|
|
5
|
+
|
|
6
|
+
id: nightly-topic-knowledge
|
|
7
|
+
name: Nightly Topic Knowledge
|
|
8
|
+
name_i18n:
|
|
9
|
+
en: Nightly Topic Knowledge
|
|
10
|
+
ja: 夜間トピックナレッジ
|
|
11
|
+
description: Distill the previous day's logs and notes into per-topic knowledge files
|
|
12
|
+
description_i18n:
|
|
13
|
+
en: Distill the previous day's logs and notes into per-topic knowledge files
|
|
14
|
+
ja: 前日の会話ログとメモをトピック別のナレッジに整理する
|
|
15
|
+
runtime: python
|
|
16
|
+
output_mode: raw
|
|
17
|
+
side_effect: true
|
|
18
|
+
|
|
19
|
+
invocation:
|
|
20
|
+
command: knowledge.nightly
|
|
21
|
+
phrases:
|
|
22
|
+
- update topic knowledge
|
|
23
|
+
- distill yesterday
|
|
24
|
+
- run nightly knowledge
|
|
25
|
+
phrases_i18n:
|
|
26
|
+
en:
|
|
27
|
+
- update topic knowledge
|
|
28
|
+
- distill yesterday
|
|
29
|
+
- run nightly knowledge
|
|
30
|
+
ja:
|
|
31
|
+
- トピックナレッジを更新
|
|
32
|
+
- 昨日の話を整理
|
|
33
|
+
- ナレッジを更新して
|
|
34
|
+
|
|
35
|
+
input:
|
|
36
|
+
schema:
|
|
37
|
+
type: object
|
|
38
|
+
additionalProperties: false
|
|
39
|
+
properties:
|
|
40
|
+
date:
|
|
41
|
+
type: string
|
|
42
|
+
pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
|
43
|
+
description: Target day in YYYY-MM-DD. Defaults to yesterday in the chosen timezone.
|
|
44
|
+
description_i18n:
|
|
45
|
+
en: Target day in YYYY-MM-DD. Defaults to yesterday in the chosen timezone.
|
|
46
|
+
ja: 対象日 (YYYY-MM-DD)。省略時はタイムゾーン基準の前日
|
|
47
|
+
model:
|
|
48
|
+
type: string
|
|
49
|
+
minLength: 1
|
|
50
|
+
description: Optional model override. Use "ollama:<name>" to call a local Ollama model directly. If omitted, the configured chat model is used.
|
|
51
|
+
description_i18n:
|
|
52
|
+
en: Optional model override. Use "ollama:<name>" to call a local Ollama model directly. If omitted, the configured chat model is used.
|
|
53
|
+
ja: モデル上書き(任意)。"ollama:<name>" でローカル Ollama を直接呼ぶ。省略時は設定済みのチャットモデルを使う
|
|
54
|
+
timezone:
|
|
55
|
+
type: string
|
|
56
|
+
minLength: 1
|
|
57
|
+
description: IANA timezone used to compute "yesterday". Defaults to Asia/Tokyo.
|
|
58
|
+
description_i18n:
|
|
59
|
+
en: IANA timezone used to compute "yesterday". Defaults to Asia/Tokyo.
|
|
60
|
+
ja: 前日判定に使うタイムゾーン。既定は Asia/Tokyo
|
|
61
|
+
max_chars:
|
|
62
|
+
type: integer
|
|
63
|
+
minimum: 1000
|
|
64
|
+
maximum: 200000
|
|
65
|
+
description: Upper bound on the combined source text passed to the model.
|
|
66
|
+
description_i18n:
|
|
67
|
+
en: Upper bound on the combined source text passed to the model.
|
|
68
|
+
ja: モデルに渡す合算ソース本文の上限文字数
|
|
69
|
+
dry_run:
|
|
70
|
+
type: boolean
|
|
71
|
+
default: false
|
|
72
|
+
description: If true, do not write files; return the planned updates only.
|
|
73
|
+
description_i18n:
|
|
74
|
+
en: If true, do not write files; return the planned updates only.
|
|
75
|
+
ja: true ならファイルに書かず、結果だけ返す
|
|
76
|
+
|
|
77
|
+
ai_steps:
|
|
78
|
+
- id: distill
|
|
79
|
+
purpose: Distill the previous day's daily memory, conversations and notes into per-topic JSON.
|
|
80
|
+
model: chat
|
|
81
|
+
- id: skill_feedback
|
|
82
|
+
purpose: From the same sources plus the current skill catalog, propose concrete skill improvements or new-skill candidates the user can batch-apply later.
|
|
83
|
+
model: chat
|
|
84
|
+
optional: true
|
|
85
|
+
|
|
86
|
+
history:
|
|
87
|
+
read: true
|
|
88
|
+
write: true
|
|
@@ -83,6 +83,32 @@ async def run(ctx, input):
|
|
|
83
83
|
loc.t(f'Schedule ID "{schedule_id}" is already registered with different settings. Remove it first or use another ID.', f'スケジュールID "{schedule_id}" は別の内容で既に登録されています。先に削除してから追加するか、別のIDを使ってください'),
|
|
84
84
|
)
|
|
85
85
|
|
|
86
|
+
# Content-based duplicate: if a different id already holds the same
|
|
87
|
+
# cron/skill/args/description/approve combination, treat as idempotent
|
|
88
|
+
# so building "毎朝4時のブリーフィング" twice doesn't double-register.
|
|
89
|
+
twin = next(
|
|
90
|
+
(item for item in entries if isinstance(item, dict) and _same_schedule(item, entry)),
|
|
91
|
+
None,
|
|
92
|
+
)
|
|
93
|
+
if twin is not None:
|
|
94
|
+
twin_id = str(twin.get("id", "")).strip() or schedule_id
|
|
95
|
+
ctx.log.info(f"schedule-add: duplicate content found at id={twin_id} (skipped)")
|
|
96
|
+
disabled_note = " (disabled)" if _enabled_value(twin) is False else ""
|
|
97
|
+
return {
|
|
98
|
+
"status": "ok",
|
|
99
|
+
"title": loc.t("Already registered", "登録済み"),
|
|
100
|
+
"summary": loc.t(f"Already registered: {twin_id}: {cron} -> {skill}{disabled_note}", f"登録済みです: {twin_id}: {cron} → {skill}{disabled_note}"),
|
|
101
|
+
"outputs": {},
|
|
102
|
+
"data": {
|
|
103
|
+
"entry": twin,
|
|
104
|
+
"total": len(entries),
|
|
105
|
+
"path": _schedules_path(workspace),
|
|
106
|
+
"already_registered": True,
|
|
107
|
+
"duplicate_of": twin_id,
|
|
108
|
+
},
|
|
109
|
+
"suggestions": [],
|
|
110
|
+
}
|
|
111
|
+
|
|
86
112
|
entries.append(entry)
|
|
87
113
|
|
|
88
114
|
try:
|