miutti's picture
Source: kernel, desktop app, tools (snapshot of the GitHub repo)
df41178 verified
Raw History Blame Contribute Delete
14 kB
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""dougu.py -- 先生に **道具を選ばせて**、結果を見せて、また選ばせる(道具の輪)
────────────────────────────────────────────────
なぜ作ったか
────────────────────────────────────────────────
カーネルは道具を すでに ひととおり持っている(machine / coderun / wiki / kernel)。
足りないのは **道具を選ぶ輪** だった。
いま : あなた → カーネルの記号的な探索 → 部品を並べる → 実行
ここ : あなた → 先生が道具を選ぶ → 実行 → 結果を見せる → また選ぶ → 答え
「Claude Code のようにパソコンを触らせたい」への答えがこれ。
────────────────────────────────────────────────
安全(ここが本体。道具より先に決めること)
────────────────────────────────────────────────
v1 に入れるのは **跡が残らないもの だけ**。
・読むだけの道具(時刻・電池・空き容量・フォルダの中身・辞書・Wikipedia)
・コードは coderun の **使い捨ての部屋**の中だけ(外に出られない)
入れないもの:
・ファイルを作る/動かす/消す … kernel 側の 仮想→関所→記録→取り消し を通すべき
・アプリ操作・打ち込む・押す … machine.py の危険度でいう「跡」
**先生が選べる道具に「跡」を1つも置かない**のが、この輪の安全の全部。
先生が間違えても、壊れるものが無い。
★ 頼み文の並び順
毎回同じもの(道具のいちらん・書き方・例)を **先** に、
毎回ちがうもの(これまでの経過・質問)を **最後** に置く。
llama.cpp は前と同じ頭を読み直さないので、これだけで数倍速くなる
(2026-09-06 実測: 同じ長さの頼み文で 75.6秒 → 3.3秒)。
Python 標準ライブラリのみ。
"""
import json
import os
import time
MAX_WA = 6 # 何回まで道具を使ってよいか
MAX_BYOU = 240 # 全体の持ち時間
# ---------------------------------------------------------------- 道具
def _shiraberu(a):
"""Wikipedia を引く"""
import wiki
w = (a.get("語") or a.get("word") or "").strip()
if not w:
return "語が空です"
r = wiki.ask(w, chars=1200)
if isinstance(r, dict):
return "【%s】\n%s" % (r.get("題", w), r.get("本文") or r.get("説明") or "")
return str(r or "見つかりませんでした")
def _kotoba(a):
"""手元の辞書(外に出ない)"""
w = (a.get("語") or "").strip()
if not w:
return "語が空です"
try:
import jisho
return str(jisho.hiku(w))
except Exception:
import wiki
return _shiraberu({"語": w})
def _folder(a):
"""フォルダの中身を見る(読むだけ。触らない)"""
import kernel as _k
basho = (a.get("場所") or "Desktop").strip()
try:
d = _k.place_dir(basho)
except Exception as e:
return "その場所は見られません: %s" % e
if not os.path.isdir(d):
return "%s がありません" % d
na = sorted(os.listdir(d))
na = [x for x in na if not x.startswith(".")]
atama = na[:60]
s = "%s に %d 個\n" % (basho, len(na)) + "\n".join(" " + x for x in atama)
if len(na) > len(atama):
s += "\n …ほか %d 個" % (len(na) - len(atama))
return s
def _pc(a):
"""パソコンの様子(読むだけ)"""
import machine as _m
nani = (a.get("何") or a.get("項目") or a.get("what") or "").strip()
hyou = {"いま何時": _m.m_time, "時刻": _m.m_time, "時間": _m.m_time,
"時": _m.m_time, "現在時刻": _m.m_time, "日付": _m.m_time,
"電池": _m.m_battery, "バッテリー": _m.m_battery,
"空き容量": _m.m_disk, "ディスク": _m.m_disk, "容量": _m.m_disk,
"メモリ": _m.m_memory_use, "音量": _m.m_volume,
"ネット": _m.m_wifi, "WiFi": _m.m_wifi, "wifi": _m.m_wifi,
"開いているアプリ": _m.m_apps, "アプリ": _m.m_apps}
f = hyou.get(nani)
if not f:
# ★ ここで「案内」だけ返すと、**先生はそれを答えだと思って作り話をする。**
# 実際に起きた(2026-09-06): {"何":"時"} が表に無く案内を返したら、
# 先生は「10:30」と答えた。本当は 18:53 だった。
# 道具が答えなかったことは、**答えなかったと はっきり言う。**
return ("【この道具は答えていません】「%s」は見られません。"
"見られるのは: %s / この結果から答えを作らないこと。"
% (nani, " / ".join(sorted(set(hyou)))))
try:
return str(f({}))
except Exception as e:
return "見られませんでした: %s" % e
def _code(a):
"""使い捨ての部屋で Python を走らせる。部屋の外には出られない"""
import coderun
src = a.get("コード") or a.get("code") or ""
if not src.strip():
return "コードが空です"
r = coderun.run(src, lang="python", timeout=15)
out = (r.get("出力") or "").strip()
err = (r.get("エラー") or "").strip()
s = out if out else "(何も出力されませんでした)"
if err:
s += "\n── エラー ──\n" + err[:600]
return s[:2000]
DOUGU = {
"しらべる": (_shiraberu, '{"語":"富士山"}', "Wikipedia を引く(外に出る)"),
"ことばの意味": (_kotoba, '{"語":"都道府県"}', "辞書を引く"),
"フォルダを見る": (_folder, '{"場所":"Desktop"}',
"中身を並べる。見るだけで触らない(Desktop/Downloads/Documents)"),
"パソコンの様子": (_pc, '{"何":"空き容量"}',
"いま何時/電池/空き容量/メモリ/音量/ネット/開いているアプリ"),
"けいさん": (_code, '{"コード":"print(sum(range(1,11)))"}',
"Python を使い捨ての部屋で走らせて出力を見る。計算や文字数えに使う"),
}
# ---------------------------------------------------------------- 頼み文
def _atama():
"""毎回おなじ部分。**必ず先頭に置く**(読み直させないため)"""
p = ["あなたはパソコンの助手です。道具を使って質問に答えます。",
"", "【使える道具】"]
for n, (_f, rei, setsu) in DOUGU.items():
p.append(' %s … %s' % (n, setsu))
p.append(' 例: {"道具":"%s","引数":%s}' % (n, rei))
p += ["",
"【答えかた】JSON をひとつだけ書く。説明もコードフェンスも書かない。",
' 道具を使うとき : {"道具":"…","引数":{…}}',
' 答えが出たとき : {"答え":"…"}',
"",
"【守ること】",
" ・分かっているつもりで答えない。**確かめてから答える**",
" ・道具の結果に無いことは書かない。無ければ「分かりません」と答える",
" ・道具が【この道具は答えていません】と言ったら、**それは答えではない**。",
" 別の引数で言い直すか、分かりませんと答えること。数字を作らないこと。",
" ・同じ道具を同じ引数で二度使わない",
" ・答えが出たら すぐ {\"答え\":…} を返す",
""]
return "\n".join(p)
def _shippo(toi, keika):
"""毎回ちがう部分。**必ず最後に置く**"""
p = []
if keika:
p.append("【これまで】")
for i, (d, hiki, res) in enumerate(keika, 1):
p.append(" %d. %s %s" % (i, d, json.dumps(hiki, ensure_ascii=False)))
p.append(" → " + (res or "")[:700].replace("\n", "\n "))
p.append("")
p.append("【質問】" + toi)
return "\n".join(p)
# ---------------------------------------------------------------- 輪
def mawasu(toi, cfg, iu=None, max_wa=MAX_WA, timeout=90):
"""道具の輪を回す。戻り値: {"答え","経過","回数","ミリ秒","error"}"""
import sensei
import teachers
t0 = time.monotonic()
keika = []
# ★ 同じ道具を同じ引数で繰り返す癖がある。**言葉で禁じても守らない。**
# 実測(2026-09-06): 「デスクトップに何がありますか」で
# 「フォルダを見る{場所:Desktop}」を **6回** 繰り返した(35秒)。
# 頼み文に「二度使わない」と書いてあっても、である。
# → 仕組みで止める。同じ手は二度走らせず、走らせたことにもしない。
tsukatta = set()
kurikaeshi = 0
for wa in range(1, max_wa + 1):
if time.monotonic() - t0 > MAX_BYOU:
return {"答え": "", "経過": keika, "回数": wa - 1,
"ミリ秒": int((time.monotonic() - t0) * 1000),
"error": "時間切れ"}
r = sensei.kiku(_atama() + _shippo(toi, keika), cfg, timeout=timeout,
system="JSON をひとつだけ返す係です。説明を書かないこと。")
if r.get("error"):
return {"答え": "", "経過": keika, "回数": wa - 1,
"ミリ秒": int((time.monotonic() - t0) * 1000),
"error": r["error"]}
j = teachers.extract_json(r["答え"])
if not isinstance(j, dict):
j = {"答え": r["答え"]} # JSON にできないなら、そのまま答えとみなす
if "答え" in j and not j.get("道具"):
return {"答え": str(j["答え"]), "経過": keika, "回数": wa - 1,
"ミリ秒": int((time.monotonic() - t0) * 1000), "error": None}
na = str(j.get("道具") or "").strip()
hiki = j.get("引数") if isinstance(j.get("引数"), dict) else {}
if na not in DOUGU:
keika.append((na or "(道具名なし)", hiki,
"そんな道具はありません。使えるのは: "
+ " / ".join(DOUGU)))
continue
kagi = na + "|" + json.dumps(hiki, ensure_ascii=False, sort_keys=True)
if kagi in tsukatta:
kurikaeshi += 1
if iu:
iu(" %d回目: %s(同じ手なので走らせません)" % (wa, na))
if kurikaeshi >= 2:
break # 二度言っても直らない。打ち切って答えさせる
keika.append((na, hiki,
"※ その手は もう使いました。結果は上に出ています。"
"答えが出ているなら {\"答え\":…} を返してください。"
"足りないなら **別の道具か 別の引数** を使ってください。"))
continue
tsukatta.add(kagi)
if iu:
iu(" %d回目: %s %s" % (wa, na, json.dumps(hiki, ensure_ascii=False)[:60]))
try:
res = DOUGU[na][0](hiki)
except Exception as e:
res = "道具が失敗しました: %s: %s" % (type(e).__name__, e)
keika.append((na, hiki, str(res)))
# 打ち切り。分かった範囲で答えさせる。
# ★ **道具の結果を 捨てないこと。**
# まとめの1回が失敗しても、調べた中身は既に手元にある。
# 実際に起きた(2026-09-06): モデルが JSON を書いている途中で切れ、
# llama-server の解析器が転んで HTTP 500 を返した
# W common_chat_peg_parse: unparsed peg-native output: {"答え":"1
# そのとき「だめでした」とだけ言って、取れていたフォルダの中身を捨てていた。
# 長い答えほど切れやすいので、**短く答えろ**と言っておく。
r = sensei.kiku(_atama() + _shippo(toi, keika)
+ "\n道具はもう使えません。分かった範囲で "
'{"答え":"…"} だけを、**200字以内で**返してください。'
"分からなければ そう答えてください。",
cfg, timeout=timeout,
system="JSON をひとつだけ返す係です。短く答えること。")
j = teachers.extract_json(r.get("答え") or "") or {}
kotae = str(j.get("答え") or r.get("答え") or "").strip()
if not kotae and keika:
# まとめに失敗した。せめて **最後に道具が返したもの** を そのまま見せる
na, hiki, res = keika[-1]
for k in reversed(keika):
if not str(k[2]).startswith("※"):
na, hiki, res = k
break
kotae = ("うまくまとめられませんでした。道具(%s)が返したものを"
"そのまま出します:\n%s" % (na, str(res)[:800]))
return {"答え": kotae, "経過": keika, "回数": len(keika),
"ミリ秒": int((time.monotonic() - t0) * 1000), "error": None}
return {"答え": kotae, "経過": keika,
"回数": len(keika), "ミリ秒": int((time.monotonic() - t0) * 1000),
"error": r.get("error") if not kotae else None}