record-workshop-dialogue-r13.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. """Append an explicitly simulated conversation, preserving the exact r13 handbook prompts."""
  2. import argparse
  3. import concurrent.futures
  4. import datetime
  5. import hashlib
  6. import html
  7. import importlib.util
  8. import json
  9. import shutil
  10. import urllib.error
  11. import urllib.request
  12. from decimal import Decimal
  13. from pathlib import Path
  14. ROOT=Path(__file__).resolve().parents[1]
  15. BASE=ROOT/'docs/workshop-quality-r13-20261110'
  16. OUT=ROOT/'docs/workshop-dialogue-r13-20261110'
  17. WORK=ROOT/'work/workshop-dialogue-r13-20261110'
  18. sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
  19. dump=lambda p,d:p.write_text(json.dumps(d,ensure_ascii=False,indent=2)+'\n',encoding='utf8')
  20. NOTICE='本记录由同一助手模拟员工和研究助手,按r13手册的15节顺序推进。员工的目标、回答、取舍与质询均为模拟,不是真实员工输入;工具执行另有原始回执。不是豆包客户端或独立双模型实测。只在本地保存,不写业务库、不建企业账套。'
  21. def read_json(p):return json.loads(p.read_bytes())
  22. def initialize():
  23. OUT.mkdir(exist_ok=False)
  24. (OUT/'evidence').mkdir()
  25. (OUT/'阶段产出').mkdir()
  26. data=read_json(BASE/'prompts-and-outputs.json')
  27. shutil.copyfile(BASE/'prompts-and-outputs.json',OUT/'使用的手册提示词.json')
  28. shutil.copyfile(BASE/'证据与编码-r13.json',OUT/'使用的证据与编码.json')
  29. meta={'schema':'ksf.workshop-simulated-dialogue.v1','version':'r13-local-quality','notice':NOTICE,
  30. 'startedAt':datetime.datetime.now(datetime.timezone(datetime.timedelta(hours=8))).isoformat(),
  31. 'manual':str(BASE/'康师傅工作坊操作手册-r13本地修订.html'),
  32. 'manualSha256':sha(BASE/'康师傅工作坊操作手册-r13本地修订.html'),
  33. 'skillRoot':str(BASE/'installation/skills/ksf-workshop-runtime'),
  34. 'skillFiles':read_json(BASE/'installation/release.json')['workshop']['files'],
  35. 'promptsSha256':sha(OUT/'使用的手册提示词.json'),
  36. 'evidenceLedgerSha256':sha(OUT/'使用的证据与编码.json'),
  37. 'order':[s['id'] for s in data['steps']],
  38. 'simulation':True,'employeeStatementsAreSimulated':True,'independentModels':False,
  39. 'actualHostAutoLoading':False,'productionWrites':0,'completedStages':[]}
  40. dump(OUT/'运行说明.json',meta)
  41. (OUT/'对话原始记录.jsonl').write_text('',encoding='utf8')
  42. print(json.dumps({'initialized':True,'root':str(OUT)},ensure_ascii=False))
  43. def messages():
  44. return [json.loads(line) for line in (OUT/'对话原始记录.jsonl').read_text('utf8').splitlines() if line]
  45. def add(stage,role,content,kind='dialogue',**extra):
  46. row={'seq':len(messages())+1,'stage':stage,'role':role,'kind':kind,'content':content,
  47. 'simulation':role!='execution',**extra}
  48. with (OUT/'对话原始记录.jsonl').open('a',encoding='utf8') as f:f.write(json.dumps(row,ensure_ascii=False)+'\n')
  49. return row
  50. def probe(name,body):
  51. assert body['op'] in {'contract','version','browse','detail'}
  52. path=OUT/'evidence'/(name+'.json')
  53. if path.exists():return read_json(path)
  54. req=urllib.request.Request('https://kangshifu.fmode.cn/api/library',data=json.dumps(body,ensure_ascii=False).encode(),
  55. headers={'Content-Type':'application/json'},method='POST')
  56. try:
  57. try:response=urllib.request.urlopen(req,timeout=25)
  58. except urllib.error.HTTPError as exc:response=exc
  59. raw=response.read();status=response.code
  60. try:d=json.loads(raw)
  61. except ValueError:d={'ok':False,'error':'NON_JSON_RESPONSE'}
  62. except (OSError,urllib.error.URLError,TimeoutError) as exc:
  63. raw=b'';status=None;d={'ok':False,'error':type(exc).__name__}
  64. path.write_bytes(raw if raw else json.dumps(d).encode())
  65. receipt={'name':name,'request':body,'url':req.full_url,'httpStatus':status,
  66. 'checkedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),
  67. 'responseSha256':sha(path),'ok':d.get('ok'),'generation':d.get('generation'),
  68. 'readOnly':d.get('readOnly'),'databaseWrites':d.get('databaseWrites'),'error':d.get('error')}
  69. dump(OUT/'evidence'/(name+'.request.json'),receipt)
  70. if d.get('ok'):assert d.get('readOnly') is True and d.get('databaseWrites')==0
  71. return d
  72. def stage_start(stage):
  73. meta=read_json(OUT/'运行说明.json');index=len(meta['completedStages'])
  74. assert meta['order'][index]==stage,(stage,index)
  75. s=next(s for s in read_json(OUT/'使用的手册提示词.json')['steps'] if s['id']==stage)
  76. add(stage,'employee',s['fullPrompt'],kind='handbook-prompt',title=s['title'],
  77. promptSha256=hashlib.sha256(s['fullPrompt'].encode()).hexdigest())
  78. def append_stage(stage):
  79. from workshop_dialogue_r13_content import TURNS
  80. stage_start(stage)
  81. if stage=='install':
  82. receipt=read_json(WORK/'install-result/install-receipt.json')
  83. assert receipt['ok'] and receipt['filesInstalled']
  84. shutil.copyfile(WORK/'install-result/install-receipt.json',OUT/'evidence/install-receipt.json')
  85. add(stage,'execution','实际在work下的新隔离目录执行r13安装器;安装回执见evidence/install-receipt.json。既有宿主技能目录未改。',kind='actual-execution')
  86. if stage=='category':
  87. # Two independent read-only capability checks; the content run reuses pinned local research material.
  88. jobs=[('library-version',{'op':'version'}),('library-contract',{'op':'contract'})]
  89. with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
  90. results=list(pool.map(lambda job:probe(*job),jobs))
  91. add(stage,'execution','实际只读核查当前资料入口。'+json.dumps([{'name':n,'ok':d.get('ok'),'generation':d.get('generation'),'error':d.get('error')} for (n,_),d in zip(jobs,results)],ensure_ascii=False),kind='actual-execution')
  92. if stage=='research':
  93. gen=read_json(OUT/'使用的证据与编码.json')['generation']
  94. jobs=[('mixed-products',dict(op='browse',entity='products',filters={'brand':'ksf','category':'banmian'},keyword='混合',limit=50,generation=gen)),
  95. ('four-bowls',dict(op='browse',entity='products',filters={'brand':'ksf','category':'banmian'},keyword='4碗',limit=50,generation=gen))]
  96. with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:results=list(pool.map(lambda job:probe(*job),jobs))
  97. add(stage,'execution','按T1实际追加两项只读补查。'+json.dumps([{'name':n,'ok':d.get('ok'),'matched':d.get('totalMatched'),'returned':len(d.get('records',[])),'generation':d.get('generation'),'error':d.get('error')} for (n,_),d in zip(jobs,results)],ensure_ascii=False),kind='actual-execution')
  98. for name in ['existing-two-flavour','existing-custom-six']:
  99. r=read_json(OUT/'evidence'/(name+'.request.json'));assert r['ok']
  100. add(stage,'execution','从补查命中实际精确回读商品:'+json.dumps({'request':r['request'],'ok':r['ok'],'generation':r['generation'],'readOnly':r['readOnly'],'databaseWrites':r['databaseWrites']},ensure_ascii=False),kind='actual-execution')
  101. if stage=='feasibility':
  102. values={'R0':'20','V0':'15','Rn':'21','Vn':'15.5','F':'1000','Qn':'100','Qc':'1000'}
  103. v={k:Decimal(n) for k,n in values.items()}
  104. u0=v['R0']-v['V0'];un=v['Rn']-v['Vn'];du=un-u0
  105. value=v['Qn']*un+v['Qc']*du-v['F']
  106. assert value==50 and du==Decimal('.5')
  107. dump(OUT/'evidence/假设算术复算.json',{'inputs':values,'empiricalCompanyData':False,'unit':'元/六碗销售单位;非报价或销量预测',
  108. 'result':{'u0':str(u0),'un':str(un),'deltaUnit':str(du),'incrementalContribution':str(value),'sameVolumeBreakEven':2000}})
  109. add(stage,'execution','实际本地复算一组明确为假设的数字,结果见evidence/假设算术复算.json;没有获取企业成本或交易数据。',kind='actual-execution')
  110. for row in TURNS[stage]:
  111. add(stage,row['role'],row['content'],kind=row.get('kind','dialogue'))
  112. meta=read_json(OUT/'运行说明.json');meta['completedStages'].append(stage);dump(OUT/'运行说明.json',meta)
  113. out=[r['content'] for r in messages() if r['stage']==stage and r['kind']=='stage-output']
  114. assert out,stage
  115. (OUT/'阶段产出'/f'{len(meta["completedStages"]):02}-{stage}.txt').write_text('\n\n'.join(out),encoding='utf-8-sig')
  116. render()
  117. print(json.dumps({'stage':stage,'messages':len(messages()),'stagesCompleted':len(meta['completedStages'])},ensure_ascii=False))
  118. def render():
  119. meta=read_json(OUT/'运行说明.json');rows=messages();steps={s['id']:s for s in read_json(OUT/'使用的手册提示词.json')['steps']}
  120. text=['康师傅工作坊|r13 本地模拟对话','',NOTICE,'','员工的回答是本次模拟设定,不是企业事实;本地引用键不是看板编号。原文提及/观察价不代表总体需求率/企业净收入。','']
  121. roles={'employee':'员工(模拟)','assistant':'助手(模拟)','execution':'执行记录(实际)'}
  122. blocks=[];last=None
  123. for r in rows:
  124. if last!=r['stage']:
  125. title=steps[r['stage']]['number']+' '+steps[r['stage']]['title']
  126. text+=['','='*35,title,'='*35,'']
  127. blocks.append('<h2 id="'+r['stage']+'">'+html.escape(title)+'</h2>');last=r['stage']
  128. label=f'{r["seq"]:03} {roles[r["role"]]}'
  129. if r['kind']=='handbook-prompt':label+='|粘贴手册原文'
  130. if r['kind']=='stage-output':label+='|本节产出'
  131. text += [label,r['content'],'']
  132. blocks.append('<div class="message '+r['role']+'"><b>'+html.escape(label)+'</b><div class="body">'+html.escape(r['content'])+'</div></div>')
  133. (OUT/'完整模拟对话.txt').write_text('\n'.join(text),encoding='utf-8-sig')
  134. page='''<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>康师傅工作坊|本地模拟对话</title><style>body{max-width:960px;margin:24px auto;padding:0 18px;font:15px/1.8 "Microsoft YaHei",sans-serif;color:#222}h1{font-size:23px}h2{font-size:18px;margin:35px 0 15px;border-top:2px solid #444;padding-top:12px}.note{color:#666;font-size:13px}.message{padding:14px 0;border-bottom:1px solid #ddd}.message.employee{background:#f7f7f7;padding:14px 12px}.message.execution{font-size:12px;color:#666}.body{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:7px}a{color:#1458a2}b{font-size:13px}@media(max-width:600px){body{margin:14px auto;font-size:14px}}</style><h1>康师傅工作坊|r13 本地模拟对话</h1><p class="note">'''+html.escape(NOTICE)+'''</p><p><a href="完整模拟对话.txt">纯文本记录</a> · <a href="对话原始记录.jsonl">逐条原始记录</a></p>'''+''.join(blocks)+'</html>'
  135. (OUT/'完整模拟对话.html').write_text(page,encoding='utf8')
  136. if __name__=='__main__':
  137. p=argparse.ArgumentParser();p.add_argument('stage');a=p.parse_args()
  138. if a.stage=='init':initialize()
  139. elif a.stage=='render':render()
  140. else:append_stage(a.stage)