Spaces:
Running
Running
Deploy cff9def from GitHub Actions
Browse files- __pycache__/agent_contract.cpython-312.pyc +0 -0
- __pycache__/agent_runtime.cpython-312.pyc +0 -0
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/feature_program.cpython-312.pyc +0 -0
- __pycache__/public_agent.cpython-312.pyc +0 -0
- __pycache__/semantic_model.cpython-312.pyc +0 -0
- __pycache__/upload_predict.cpython-312.pyc +0 -0
- agent_runtime.py +9 -0
- public_agent.py +323 -37
- requirements.txt +4 -3
- web/agent-ui.js +9 -3
- web/demo-chat.js +5 -5
__pycache__/agent_contract.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/agent_contract.cpython-312.pyc and b/__pycache__/agent_contract.cpython-312.pyc differ
|
|
|
__pycache__/agent_runtime.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/agent_runtime.cpython-312.pyc and b/__pycache__/agent_runtime.cpython-312.pyc differ
|
|
|
__pycache__/app.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-312.pyc and b/__pycache__/app.cpython-312.pyc differ
|
|
|
__pycache__/feature_program.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/feature_program.cpython-312.pyc and b/__pycache__/feature_program.cpython-312.pyc differ
|
|
|
__pycache__/public_agent.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/public_agent.cpython-312.pyc and b/__pycache__/public_agent.cpython-312.pyc differ
|
|
|
__pycache__/semantic_model.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/semantic_model.cpython-312.pyc and b/__pycache__/semantic_model.cpython-312.pyc differ
|
|
|
__pycache__/upload_predict.cpython-312.pyc
CHANGED
|
Binary files a/__pycache__/upload_predict.cpython-312.pyc and b/__pycache__/upload_predict.cpython-312.pyc differ
|
|
|
agent_runtime.py
CHANGED
|
@@ -39,6 +39,10 @@ class SessionRuntime:
|
|
| 39 |
def command(self, name, args):
|
| 40 |
return ""
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def _emit(self, **e):
|
| 43 |
if e["type"] not in ("token", "reasoning"):
|
| 44 |
log.info("session %s %s %s", self.id, e["type"], e.get("tool") or e.get("state") or "")
|
|
@@ -98,6 +102,11 @@ class SessionRuntime:
|
|
| 98 |
self.usage = result.context_wrapper.usage
|
| 99 |
if result.interruptions:
|
| 100 |
self.state = result.to_state()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
return dict(type="approval", actions=self.pending())
|
| 102 |
self.state = None
|
| 103 |
return dict(type="status", state="finished")
|
|
|
|
| 39 |
def command(self, name, args):
|
| 40 |
return ""
|
| 41 |
|
| 42 |
+
def _prepare_approval(self, actions):
|
| 43 |
+
"""Adapter hook for binding an approval to its displayed tool inputs."""
|
| 44 |
+
pass
|
| 45 |
+
|
| 46 |
def _emit(self, **e):
|
| 47 |
if e["type"] not in ("token", "reasoning"):
|
| 48 |
log.info("session %s %s %s", self.id, e["type"], e.get("tool") or e.get("state") or "")
|
|
|
|
| 102 |
self.usage = result.context_wrapper.usage
|
| 103 |
if result.interruptions:
|
| 104 |
self.state = result.to_state()
|
| 105 |
+
try:
|
| 106 |
+
self._prepare_approval(self.pending())
|
| 107 |
+
except Exception:
|
| 108 |
+
self.state = None
|
| 109 |
+
raise
|
| 110 |
return dict(type="approval", actions=self.pending())
|
| 111 |
self.state = None
|
| 112 |
return dict(type="status", state="finished")
|
public_agent.py
CHANGED
|
@@ -4,6 +4,7 @@ internal demo. Only the data and result-store adapters differ in this deployment
|
|
| 4 |
"""
|
| 5 |
from __future__ import annotations
|
| 6 |
import asyncio
|
|
|
|
| 7 |
import json
|
| 8 |
import os
|
| 9 |
import queue
|
|
@@ -127,20 +128,30 @@ def describe(task='all'):
|
|
| 127 |
return {'datasets':out}
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
def ontology(query):
|
| 131 |
if not isinstance(query,str) or len(query)>4000 or not re.search(r'\b(SELECT|ASK)\b',query,re.I) or re.search(r'\b(SERVICE|FROM|LOAD|INSERT|DELETE|CREATE|DROP|CLEAR|MOVE|COPY|ADD)\b',query,re.I):
|
| 132 |
raise ValueError('Read-only local SELECT/ASK queries only')
|
| 133 |
g=rdflib.Graph();d=rdflib.Namespace('urn:ontic:data:');o=rdflib.Namespace('urn:ontic:')
|
| 134 |
g.bind('d',d);g.bind('ontic',o);g.bind('rdfs',rdflib.RDFS);g.bind('owl',rdflib.OWL);g.bind('xsd',rdflib.XSD)
|
| 135 |
for key,spec in DATA.items():
|
|
|
|
|
|
|
| 136 |
t=d[key];g.add((t,rdflib.RDF.type,rdflib.OWL.Class));g.add((t,rdflib.RDFS.label,rdflib.Literal(spec['name'])))
|
| 137 |
for i,title in enumerate(spec['sheets'][0]['columns']):
|
| 138 |
c=d[f'{key}.column{i}'];g.add((c,rdflib.RDFS.domain,t));g.add((c,rdflib.RDFS.label,rdflib.Literal(title)))
|
| 139 |
g.add((c,rdflib.RDFS.range,rdflib.XSD.string if i==0 else rdflib.XSD.double))
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
|
| 146 |
def llm_features(task,n=4,token=None,requirements=""):
|
|
@@ -153,9 +164,12 @@ When the user names specific columns, use ONLY those columns and do not add othe
|
|
| 153 |
Use only declared c0/c1/c2/c3 columns. Never use entity_id or labels.
|
| 154 |
Format: {{"programs":[{{"name":"snake_case","path":["{task}"],"agg":"none","expr":"c0","window_days":null,"rationale":"ํ๊ตญ์ด ๊ทผ๊ฑฐ"}}]}}.
|
| 155 |
These are snapshots with no time history. agg must be none and window_days null.
|
|
|
|
| 156 |
Include relevant source columns and optionally defensible arithmetic expressions if requested. No fictitious time aggregations or units. JSON object with programs array only.'''
|
| 157 |
schema={'type':'object','properties':{'programs':{'type':'array','minItems':1,'maxItems':n,'items':{'type':'object','properties':{'name':{'type':'string'},'path':{'type':'array','items':{'type':'string','enum':[task]},'minItems':1,'maxItems':1},'agg':{'type':'string','enum':['none']},'expr':{'type':'string'},'window_days':{'type':'null'},'rationale':{'type':'string'}},'required':['name','path','agg','expr','window_days','rationale'],'additionalProperties':False}}},'required':['programs'],'additionalProperties':False}
|
| 158 |
-
|
|
|
|
|
|
|
| 159 |
text=response.choices[0].message.content or ''
|
| 160 |
try: programs=json.loads(text)['programs']
|
| 161 |
except Exception:raise ValueError('Feature proposal was not a programs object; retry propose_features')
|
|
@@ -179,15 +193,82 @@ def columns(task):
|
|
| 179 |
return {f'c{i}':title for i,title in enumerate(DATA[task]['sheets'][0]['columns'][1:5])}
|
| 180 |
|
| 181 |
|
| 182 |
-
def
|
| 183 |
-
"""
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
|
| 193 |
def observe(ctx,name,fn):
|
|
@@ -242,11 +323,23 @@ def show_view(ctx:RunContextWrapper,kind:Literal['schema','table','distribution'
|
|
| 242 |
|
| 243 |
def describe_data(ctx:RunContextWrapper,task:Literal['all','churn','stockout','defect']='all',summary:str='')->str:
|
| 244 |
"""Read actual workbook schemas, column names, row counts, statistics and ontology. task: all|churn|stockout|defect. summary: short Korean status."""
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
|
| 248 |
def query_ontology(ctx:RunContextWrapper,query:str,summary:str='')->str:
|
| 249 |
-
"""Read
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
return observe(ctx,'query_ontology',lambda:ontology(query))
|
| 251 |
|
| 252 |
|
|
@@ -254,22 +347,64 @@ def propose_features(ctx:RunContextWrapper,task:Literal['churn','stockout','defe
|
|
| 254 |
"""Use the LLM to synthesize feature programs, then validate against the ontology. task: churn|stockout|defect, n:1-8. summary: Korean status."""
|
| 255 |
def run():
|
| 256 |
if not 1<=n<=8:raise ValueError('n must be 1-8')
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
ctx.context.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
return observe(ctx,'propose_features',run)
|
| 261 |
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
def execute_features(ctx:RunContextWrapper,task:Literal['churn','stockout','defect'],names:list[str]|None=None,summary:str='')->str:
|
| 264 |
"""Execute proposed features after the system obtains user approval. Empty names means all accepted proposals. summary: Korean status."""
|
| 265 |
def run():
|
| 266 |
-
s=ctx.context;
|
| 267 |
-
if not proposal:raise ValueError('Call propose_features first')
|
| 268 |
-
programs=proposal['programs']
|
| 269 |
-
if names:
|
| 270 |
-
if set(names)-{p['name'] for p in programs}:raise ValueError('Unknown proposed feature')
|
| 271 |
-
programs=[p for p in programs if p['name'] in names]
|
| 272 |
-
if not programs:raise ValueError('No approved feature programs')
|
| 273 |
frame,y,subjects,sm=tables(task)
|
| 274 |
fps=[FeatureProgram.from_dict(p) for p in programs]
|
| 275 |
for fp in fps:
|
|
@@ -312,30 +447,64 @@ def predict(ctx:RunContextWrapper,feat_id:str,summary:str='')->str:
|
|
| 312 |
prob=tabfm(X[:len(y)],y,X[len(y):])
|
| 313 |
rows=sorted([{'row':i,'subject':subject,'probability':float(p)} for i,(subject,p) in enumerate(zip(f['subjects'],prob))],key=lambda r:-r['probability'])
|
| 314 |
run_id=secrets.token_hex(16)
|
|
|
|
| 315 |
result={'run_id':run_id,'task':f['task'],'features':[p['name'] for p in f['programs']],'programs':f['programs'],'predictions':rows,'validation':{'method':'stratified synthetic holdout, 72 train / 24 validation','auc':auc,'constant_baseline_auc':0.5,'baseline_probability':baseline},'seconds':round(time.monotonic()-started,3),'model':{'repo':'jingang/TabICL','checkpoint':CHECKPOINT,'output':'real_inference'},'columns':columns(f['task']),'caveat':'Synthetic validation only; target outcomes unknown. No causal interpretation or production accuracy claim.'}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
ctx.context.runs[run_id]=dict(result,feat_id=feat_id);return result
|
| 317 |
out=observe(ctx,'predict',run);result=json.loads(out)
|
| 318 |
if 'run_id' not in result:return out
|
| 319 |
-
#
|
| 320 |
-
|
| 321 |
-
return
|
| 322 |
|
| 323 |
|
| 324 |
def explain_once(ctx,run_id,row):
|
| 325 |
"""Emit evidence for a (run, row) once; a repeated request (e.g. the LLM after the automatic one) returns it silently."""
|
| 326 |
seen=ctx.context.__dict__.setdefault('explained',{})
|
| 327 |
if (run_id,row) in seen:return json.dumps(seen[run_id,row],ensure_ascii=False)
|
| 328 |
-
|
|
|
|
|
|
|
| 329 |
if 'error' not in data:seen[run_id,row]=data
|
| 330 |
return out
|
| 331 |
|
| 332 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
def evidence(s,run_id,row):
|
| 334 |
r=s.runs.get(run_id)
|
| 335 |
if not r:raise ValueError('Unknown prediction run in this session')
|
| 336 |
-
|
| 337 |
-
if row<0 or row>=len(f['subjects']):raise ValueError('Unknown target row')
|
| 338 |
-
return {'subject':f['subjects'][row],'row':row,'features':[{'feature':p['name'],'expr':p['expr'],'rationale':p['rationale'],'value':float(f['matrix'][96+row,i])} for i,p in enumerate(f['programs'])],'caveat':'Observed feature values, not causal effects or feature importance.'}
|
| 339 |
|
| 340 |
|
| 341 |
def explain(ctx:RunContextWrapper,run_id:str,row:int=0,summary:str='')->str:
|
|
@@ -355,7 +524,22 @@ def publish_results(ctx:RunContextWrapper,run_id:str,summary:str='')->str:
|
|
| 355 |
return {'run_id':run_id,'dashboard_url':'/demo#report='+run_id,'n_rows':len(result['predictions']),'storage':'public synthetic result database, expires after 24 hours or backend restart'}
|
| 356 |
return observe(ctx,'publish_results',run)
|
| 357 |
|
| 358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
DEPLOYMENT_NOTES='''
|
| 360 |
PUBLIC DATA ADAPTER (same responsibilities, different data source):
|
| 361 |
Available tasks are churn (๊ณ ๊ฐ ์ดํ), stockout (์ฌ๊ณ ๋ถ์กฑ), defect (์์ฐ ๋ถ๋); describe_data(task="all") lists actual downloaded workbook columns.
|
|
@@ -363,21 +547,92 @@ Pass requested columns or transformations to propose_features(requirements=...).
|
|
| 363 |
These three public datasets are synthetic snapshots, NOT the internal rel-f1 database. There is no Superset MCP in this deployment; publish_results writes the actual result database and provides a report URL. Do not claim a Superset dashboard, production evaluation or official test split. predict supplies synthetic holdout AUROC and a constant baseline, with unknown target outcomes. Features have no physical units.
|
| 364 |
When the user asks for a picture, diagram, chart, graph, distribution or schema, call show_view and add at most two sentences; never draw ASCII boxes, tables in code blocks or text diagrams. Never show tool names, function-call syntax or parameters to the user, and never describe a view or capability that show_view does not have; if nothing fits, say so plainly in Korean.
|
| 365 |
For a prediction, keep the original seven-step workflow and execute_features approval pause. For questions or explanations, use data/ontology tools and answer directly without triggering a prediction. Name risk probabilities only from predict output. Explain input evidence and caveats using explain; predict output already includes top_evidence for the highest-risk row. Do not estimate probabilities yourself.
|
| 366 |
-
Always finish the workflow with publish_results; the screen turns predict, explain and publish_results observations into cards (ranking, top probability, evidence, report link). After publishing, answer in two or three Korean sentences:
|
| 367 |
'''
|
| 368 |
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
class PublicSession(SessionRuntime):
|
| 371 |
steps=STEPS
|
| 372 |
own_tools=set(STEPS)
|
| 373 |
def __init__(self):
|
| 374 |
-
super().__init__();self.request_text='';self.proposals={};self.features={};self.runs={};self.touched=time.monotonic()
|
| 375 |
def send(self,text):
|
| 376 |
self.request_text=text
|
| 377 |
return super().send(text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
def _agent(self,mcp_servers=()):
|
| 379 |
base,key=credentials(self.id)
|
| 380 |
-
return Agent(name='ONTIC',instructions=
|
| 381 |
|
| 382 |
SESSIONS={}
|
| 383 |
SESSIONS_LOCK=threading.Lock()
|
|
@@ -449,6 +704,34 @@ def report(run_id:str):
|
|
| 449 |
|
| 450 |
local_router=APIRouter()
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
@local_router.post('/local-llm/v1/chat/completions')
|
| 453 |
async def local_completion(request:Request):
|
| 454 |
if not request.client or request.client.host not in ('127.0.0.1','::1'):raise HTTPException(403,'Loopback only')
|
|
@@ -460,6 +743,9 @@ async def local_completion(request:Request):
|
|
| 460 |
try:result=await asyncio.to_thread(dispatch,'llm',dict(data,model=MODEL,stream=False),strict=True)
|
| 461 |
except GPUQwenUnavailable as exc:
|
| 462 |
return JSONResponse({'error':{'message':str(exc),'type':'gpu_qwen_unavailable','code':'GPU_QWEN_UNAVAILABLE','reason':exc.reason}},status_code=503)
|
|
|
|
|
|
|
|
|
|
| 463 |
if not data.get('stream'):return result
|
| 464 |
choice=result['choices'][0];message=choice['message'];delta=dict(message)
|
| 465 |
if message.get('tool_calls'):delta['tool_calls']=[dict(t,index=i) for i,t in enumerate(message['tool_calls'])]
|
|
|
|
| 4 |
"""
|
| 5 |
from __future__ import annotations
|
| 6 |
import asyncio
|
| 7 |
+
import ast
|
| 8 |
import json
|
| 9 |
import os
|
| 10 |
import queue
|
|
|
|
| 128 |
return {'datasets':out}
|
| 129 |
|
| 130 |
|
| 131 |
+
# RDFLib/pyparsing share parser state, including the first query's lazy setup.
|
| 132 |
+
# Keep query evaluation and result materialization inside one process-wide lock.
|
| 133 |
+
ONTOLOGY_QUERY_LOCK = threading.Lock()
|
| 134 |
+
|
| 135 |
+
|
| 136 |
def ontology(query):
|
| 137 |
if not isinstance(query,str) or len(query)>4000 or not re.search(r'\b(SELECT|ASK)\b',query,re.I) or re.search(r'\b(SERVICE|FROM|LOAD|INSERT|DELETE|CREATE|DROP|CLEAR|MOVE|COPY|ADD)\b',query,re.I):
|
| 138 |
raise ValueError('Read-only local SELECT/ASK queries only')
|
| 139 |
g=rdflib.Graph();d=rdflib.Namespace('urn:ontic:data:');o=rdflib.Namespace('urn:ontic:')
|
| 140 |
g.bind('d',d);g.bind('ontic',o);g.bind('rdfs',rdflib.RDFS);g.bind('owl',rdflib.OWL);g.bind('xsd',rdflib.XSD)
|
| 141 |
for key,spec in DATA.items():
|
| 142 |
+
*_,sm=tables(key)
|
| 143 |
+
policy_columns=sm.tables[key]['columns']
|
| 144 |
t=d[key];g.add((t,rdflib.RDF.type,rdflib.OWL.Class));g.add((t,rdflib.RDFS.label,rdflib.Literal(spec['name'])))
|
| 145 |
for i,title in enumerate(spec['sheets'][0]['columns']):
|
| 146 |
c=d[f'{key}.column{i}'];g.add((c,rdflib.RDFS.domain,t));g.add((c,rdflib.RDFS.label,rdflib.Literal(title)))
|
| 147 |
g.add((c,rdflib.RDFS.range,rdflib.XSD.string if i==0 else rdflib.XSD.double))
|
| 148 |
+
code='entity_id' if i==0 else f'c{i-1}'
|
| 149 |
+
sensitivity=policy_columns.get(code,{}).get('sensitivity','none')
|
| 150 |
+
g.add((c,o.sensitivity,rdflib.Literal(sensitivity)))
|
| 151 |
+
with ONTOLOGY_QUERY_LOCK:
|
| 152 |
+
r=g.query(query)
|
| 153 |
+
if r.type=='ASK':return {'answer':bool(r.askAnswer)}
|
| 154 |
+
return {'rows':[{str(v):str(row[v]) if row[v] is not None else None for v in r.vars} for row in list(r)[:100]]}
|
| 155 |
|
| 156 |
|
| 157 |
def llm_features(task,n=4,token=None,requirements=""):
|
|
|
|
| 164 |
Use only declared c0/c1/c2/c3 columns. Never use entity_id or labels.
|
| 165 |
Format: {{"programs":[{{"name":"snake_case","path":["{task}"],"agg":"none","expr":"c0","window_days":null,"rationale":"ํ๊ตญ์ด ๊ทผ๊ฑฐ"}}]}}.
|
| 166 |
These are snapshots with no time history. agg must be none and window_days null.
|
| 167 |
+
Rationale must describe the proposed input calculation, not an observed effect on risk. Do not claim feature importance, causal impact, probability amplification or measured interactions. For a ratio, keep the numerator and denominator in the same order as expr. Any usefulness claim is an unvalidated hypothesis.
|
| 168 |
Include relevant source columns and optionally defensible arithmetic expressions if requested. No fictitious time aggregations or units. JSON object with programs array only.'''
|
| 169 |
schema={'type':'object','properties':{'programs':{'type':'array','minItems':1,'maxItems':n,'items':{'type':'object','properties':{'name':{'type':'string'},'path':{'type':'array','items':{'type':'string','enum':[task]},'minItems':1,'maxItems':1},'agg':{'type':'string','enum':['none']},'expr':{'type':'string'},'window_days':{'type':'null'},'rationale':{'type':'string'}},'required':['name','path','agg','expr','window_days','rationale'],'additionalProperties':False}}},'required':['programs'],'additionalProperties':False}
|
| 170 |
+
# This bounded structured subrequest needs final JSON, not a thinking trace
|
| 171 |
+
# that can exhaust its output budget. Keep the main agent's reasoning intact.
|
| 172 |
+
response=OpenAI(base_url=base,api_key=key,timeout=120,max_retries=0).chat.completions.create(model=MODEL,response_format={'type':'json_schema','json_schema':{'name':'feature_programs','schema':schema}},messages=[{'role':'system','content':'You propose ontology-constrained feature programs. Return a JSON object containing programs.'},{'role':'user','content':prompt}],temperature=0,max_tokens=1800,reasoning_effort='none')
|
| 173 |
text=response.choices[0].message.content or ''
|
| 174 |
try: programs=json.loads(text)['programs']
|
| 175 |
except Exception:raise ValueError('Feature proposal was not a programs object; retry propose_features')
|
|
|
|
| 193 |
return {f'c{i}':title for i,title in enumerate(DATA[task]['sheets'][0]['columns'][1:5])}
|
| 194 |
|
| 195 |
|
| 196 |
+
def column_policy(task,text,previous=()):
|
| 197 |
+
"""Resolve explicit source-column constraints, including edits to a proposal.
|
| 198 |
+
|
| 199 |
+
This validates the user's column boundary; the LLM still proposes programs.
|
| 200 |
+
Relative exclusions apply to the previous proposal, not every dataset input.
|
| 201 |
+
"""
|
| 202 |
+
text=text or '';cols=columns(task);aliases={v:k for k,v in cols.items()}|{k:k for k in cols}
|
| 203 |
+
pattern='|'.join(re.escape(x) for x in sorted(aliases,key=len,reverse=True))
|
| 204 |
+
mentions=[(m.start(),m.end(),aliases[m.group()]) for m in re.finditer(pattern,text)]
|
| 205 |
+
excluded=set()
|
| 206 |
+
protected=[m.span() for m in re.finditer(r'๋นผ์ง\s*(?:๋ง|์)|์ ์ธํ์ง\s*(?:๋ง|์)|์ ์ธ\s*์\s*ํ|์\s*๋นผ',text)]
|
| 207 |
+
negative=r'์ ์ธ(?:ํด|ํ|ํ|ํ )?|๋นผ(?:๊ณ |์ค|์|๋ฉด)?|๋ง๊ณ |(?:์ฌ์ฉ|์ฐ|๋ฃ)์ง\s*(?:๋ง|๋ง|์)|์ฌ์ฉํ์ง\s*(?:๋ง|๋ง|์)|์\s*(?:์ฐ|์ฌ์ฉ|๋ฃ)'
|
| 208 |
+
negatives=[]
|
| 209 |
+
for match in re.finditer(negative,text):
|
| 210 |
+
if any(start<=match.start()<end for start,end in protected):continue
|
| 211 |
+
negatives.append(match)
|
| 212 |
+
before=[m for m in mentions if m[1]<=match.start() and match.start()-m[1]<=45
|
| 213 |
+
and not re.search(r'[.!?ใ\n]',text[m[1]:match.start()])]
|
| 214 |
+
if not before:continue
|
| 215 |
+
group=[before[-1]]
|
| 216 |
+
for candidate in reversed(before[:-1]):
|
| 217 |
+
if re.fullmatch(r'[\s,ใยท/&์๊ณผ๋ฐ์ด๋ํ๊ณ ๋๋์๋ฅผ์๋๋๋ง]+',text[candidate[1]:group[-1][0]]):group.append(candidate)
|
| 218 |
+
else:break
|
| 219 |
+
excluded.update(m[2] for m in group)
|
| 220 |
+
# Prefix phrasing names what to remove after the verb, e.g. "์ ์ธํ ์ปฌ๋ผ์ A".
|
| 221 |
+
for match in re.finditer(r'(?:์ ์ธํ |์ ์ธํ๋|๋บ|๋นผ์ผ\s*ํ )\s*(?:(?:์
๋ ฅ|์ปฌ๋ผ|์ด|๋ณ์|ํผ์ฒ|๊ธฐ์ค)\s*)?(?:์|๋|์ด|๊ฐ)?\s*[:๏ผ]?\s*',text):
|
| 222 |
+
after=[m for m in mentions if m[0]>=match.end() and m[0]-match.end()<=45]
|
| 223 |
+
if not after:continue
|
| 224 |
+
group=[after[0]]
|
| 225 |
+
for candidate in after[1:]:
|
| 226 |
+
if re.fullmatch(r'[\s,ใยท/&์๊ณผ๋ฐ์ด๋ํ๊ณ ๋๋์๋ฅผ์๋๋๋ง]+',text[group[-1][1]:candidate[0]]):group.append(candidate)
|
| 227 |
+
else:break
|
| 228 |
+
excluded.update(m[2] for m in group)
|
| 229 |
+
named={m[2] for m in mentions};positive=named-excluded
|
| 230 |
+
prior=set(re.findall(r'\bc[0-3]\b',' '.join(p.get('expr','') for p in previous)))
|
| 231 |
+
relative=bool(re.search(r'๋๋จธ์ง|๋จ์|๋จ๊ฒจ|๊ธฐ์กด|์ด์ |๋ฐฉ๊ธ',text))
|
| 232 |
+
all_mentions=list(re.finditer(r'๋ชจ๋ \s*(?:์
๋ ฅ|์ปฌ๋ผ|๋ณ์)|(?:์
๋ ฅ|์ปฌ๋ผ|๋ณ์)(?:์|๋ฅผ)?\s*(?:๋ชจ๋|์ ๋ถ)',text))
|
| 233 |
+
negated_all=any(any(a.end()<=m.start()<next((c[0] for c in mentions if c[0]>=a.end()),len(text)) for m in negatives) for a in all_mentions)
|
| 234 |
+
all_inputs=bool(all_mentions) and not negated_all
|
| 235 |
+
if all_inputs:allowed=set(cols)-excluded
|
| 236 |
+
elif negated_all:allowed=positive-excluded
|
| 237 |
+
elif excluded:allowed=(positive or prior or set(cols))-excluded
|
| 238 |
+
elif positive:allowed=positive
|
| 239 |
+
elif relative and prior:allowed=prior
|
| 240 |
+
else:allowed=None
|
| 241 |
+
if allowed is not None and not allowed:raise ValueError('No requested input columns remain; choose another input before proposing features')
|
| 242 |
+
return {'allowed':allowed,'excluded':excluded,'relative':relative}
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def restrict(task,result,text,previous=()):
|
| 246 |
+
"""Enforce positive and negative column requirements independently of the LLM."""
|
| 247 |
+
policy=column_policy(task,text,previous);allowed=policy['allowed']
|
| 248 |
+
if allowed is None:return result
|
| 249 |
+
accepted=[];rejected=list(result.get('rejected',[]))
|
| 250 |
+
for program in result['programs']:
|
| 251 |
+
extra=set(re.findall(r'\bc\d+\b',program['expr']))-allowed
|
| 252 |
+
if extra:rejected.append({'program':program,'reason':f'Uses columns the user did not request: {sorted(extra)}'})
|
| 253 |
+
else:accepted.append(program)
|
| 254 |
+
if not accepted:raise ValueError(f'Every proposal used unrequested columns; propose again using only {sorted(allowed)}')
|
| 255 |
+
return dict(result,programs=accepted,rejected=rejected)
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def feature_definition(task,expr):
|
| 259 |
+
"""Describe only validated arithmetic; a model rationale is not an effect."""
|
| 260 |
+
cols=columns(task)
|
| 261 |
+
try:node=ast.parse(expr,mode='eval').body
|
| 262 |
+
except SyntaxError:node=None
|
| 263 |
+
if isinstance(node,ast.Name) and node.id in cols:return 'ํด๋น ์ปฌ๋ผ์ ์๋ณธ ์
๋ ฅ๊ฐ'
|
| 264 |
+
if isinstance(node,ast.BinOp) and isinstance(node.left,ast.Name) and isinstance(node.right,ast.Name):
|
| 265 |
+
left,right=cols.get(node.left.id),cols.get(node.right.id)
|
| 266 |
+
if left and right:
|
| 267 |
+
if isinstance(node.op,ast.Div):return f'๋ถ์: {left} ยท ๋ถ๋ชจ: {right}'
|
| 268 |
+
if isinstance(node.op,ast.Mult):return '๋ ์
๋ ฅ๊ฐ์ ๊ณฑํ ๊ฐ'
|
| 269 |
+
if isinstance(node.op,ast.Add):return '๋ ์
๋ ฅ๊ฐ์ ๋ํ ๊ฐ'
|
| 270 |
+
if isinstance(node.op,ast.Sub):return '์ ์
๋ ฅ๊ฐ์์ ๋ค ์
๋ ฅ๊ฐ์ ๋บ ๊ฐ'
|
| 271 |
+
return 'ํ์๋ ๊ณ์ฐ์์ ์๋ณธ ์
๋ ฅ์ ์ ์ฉํ ๊ฐ'
|
| 272 |
|
| 273 |
|
| 274 |
def observe(ctx,name,fn):
|
|
|
|
| 323 |
|
| 324 |
def describe_data(ctx:RunContextWrapper,task:Literal['all','churn','stockout','defect']='all',summary:str='')->str:
|
| 325 |
"""Read actual workbook schemas, column names, row counts, statistics and ontology. task: all|churn|stockout|defect. summary: short Korean status."""
|
| 326 |
+
out=observe(ctx,'describe_data',lambda:describe(task));data=json.loads(out)
|
| 327 |
+
if 'error' in data:return out
|
| 328 |
+
# The feature synthesizer has the executable mapping separately. Ordinary
|
| 329 |
+
# data questions need source names, not its internal c0/c1 aliases.
|
| 330 |
+
for dataset in data['datasets']:
|
| 331 |
+
dataset.pop('feature_mapping',None);dataset.pop('semantic_model',None)
|
| 332 |
+
return json.dumps(data,ensure_ascii=False)
|
| 333 |
|
| 334 |
|
| 335 |
def query_ontology(ctx:RunContextWrapper,query:str,summary:str='')->str:
|
| 336 |
+
"""Read local SELECT/ASK SPARQL. Prefixes ontic:,d:,rdfs:,owl:,xsd: are predefined.
|
| 337 |
+
Tables are d:churn, d:stockout, d:defect. Columns have rdfs:domain,
|
| 338 |
+
rdfs:label, rdfs:range and ontic:sensitivity; nullRate/distinct are absent.
|
| 339 |
+
Example: SELECT ?col ?label ?sensitivity WHERE { ?col rdfs:domain d:churn; rdfs:label ?label; ontic:sensitivity ?sensitivity }
|
| 340 |
+
To select an ID use FILTER(?col = d:churn.column0), never a bare ?col = ... triple.
|
| 341 |
+
summary: Korean status.
|
| 342 |
+
"""
|
| 343 |
return observe(ctx,'query_ontology',lambda:ontology(query))
|
| 344 |
|
| 345 |
|
|
|
|
| 347 |
"""Use the LLM to synthesize feature programs, then validate against the ontology. task: churn|stockout|defect, n:1-8. summary: Korean status."""
|
| 348 |
def run():
|
| 349 |
if not 1<=n<=8:raise ValueError('n must be 1-8')
|
| 350 |
+
previous=ctx.context.proposals.get(task,{}).get('programs',[])
|
| 351 |
+
policy=column_policy(task,ctx.context.request_text,previous)
|
| 352 |
+
requested=requirements or ctx.context.request_text
|
| 353 |
+
if policy['relative'] and policy['allowed'] is not None:
|
| 354 |
+
requested+='\n์ด๋ฒ ์ ์์ ํ์ฉ๋ ์
๋ ฅ ์ปฌ๋ผ์ '+', '.join(columns(task)[c] for c in sorted(policy['allowed']))+'๋ฟ์
๋๋ค. ๋ค๋ฅธ ์
๋ ฅ์ ์ถ๊ฐํ์ง ๋ง์ธ์.'
|
| 355 |
+
result=llm_features(task,n,ctx.context.id,requested)
|
| 356 |
+
result=restrict(task,result,ctx.context.request_text,previous) # the user's own edit still binds after LLM rewriting
|
| 357 |
+
if policy['allowed'] is not None:
|
| 358 |
+
result['input_boundary']={'source':'latest_user_request_and_previous_proposal',
|
| 359 |
+
'allowed_columns':[columns(task)[c] for c in sorted(policy['allowed'])],
|
| 360 |
+
'excluded_columns':[columns(task)[c] for c in sorted(policy['excluded'])],
|
| 361 |
+
'relative_to_previous_proposal':policy['relative'],
|
| 362 |
+
'note':'์ด ๋ฒ์๋ ์ฌ์ฉ์ ์๋ฌธ์์ ๊ฒ์ฆํ์ต๋๋ค. ์
๋ ฅ์ด ํ๋๋ง ๋จ์๋ ์๊ฐ์ด๋ ํด๋น ์
๋ ฅ๋ง์ ๊ณ์ฐ์ ์ ํจํฉ๋๋ค. ๋ค๋ฅธ ์ปฌ๋ผ์ ์ถ๊ฐํ๋ ค๊ณ ๋ฐ๋ณต ์ ์ํ์ง ๋ง์ธ์. n์ ์ต๋ ๊ฐ์์
๋๋ค.'}
|
| 363 |
+
result['definitions']={p['name']:feature_definition(task,p['expr']) for p in result['programs']}
|
| 364 |
+
result['rationale_status']='model_proposal_not_empirical_effect'
|
| 365 |
+
ctx.context.proposals[task]=result;ctx.context.proposal_task=task;return result
|
| 366 |
return observe(ctx,'propose_features',run)
|
| 367 |
|
| 368 |
|
| 369 |
+
def _execution_programs(session,task,names=None):
|
| 370 |
+
"""Select the exact requested programs without widening the latest scope.
|
| 371 |
+
|
| 372 |
+
Invalid selections are rejected together; no valid subset executes silently.
|
| 373 |
+
The same check runs before SDK approval and again before feature execution.
|
| 374 |
+
"""
|
| 375 |
+
proposal=session.proposals.get(task)
|
| 376 |
+
if not proposal:raise ValueError('Call propose_features first')
|
| 377 |
+
programs=proposal['programs']
|
| 378 |
+
if names is not None and (not isinstance(names,list) or any(not isinstance(name,str) for name in names)):
|
| 379 |
+
raise ValueError('Feature names must be a list of proposed names')
|
| 380 |
+
if names and (len(names)!=len(set(names)) or set(names)-{p['name'] for p in programs}):
|
| 381 |
+
raise ValueError('Unknown or duplicate proposed feature names')
|
| 382 |
+
selected=[p for p in programs if not names or p['name'] in names]
|
| 383 |
+
if not selected:raise ValueError('No approved feature programs')
|
| 384 |
+
allowed=column_policy(task,session.request_text,programs)['allowed']
|
| 385 |
+
def within_scope(program):
|
| 386 |
+
return allowed is None or not (set(re.findall(r'\bc\d+\b',program['expr']))-allowed)
|
| 387 |
+
invalid=[p['name'] for p in selected if not within_scope(p)]
|
| 388 |
+
if invalid:
|
| 389 |
+
remaining=[p['name'] for p in programs if within_scope(p)]
|
| 390 |
+
raise ValueError(f'Programs violate latest requested input columns: {invalid}. '
|
| 391 |
+
f'Propose again or request fresh approval for remaining names: {remaining}')
|
| 392 |
+
return json.loads(json.dumps(selected,ensure_ascii=False))
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
async def execution_needs_approval(ctx,arguments,call_id):
|
| 396 |
+
"""Only valid selections reach approval; invalid ones emit tool errors."""
|
| 397 |
+
try:
|
| 398 |
+
_execution_programs(ctx.context,arguments.get('task'),arguments.get('names'))
|
| 399 |
+
except ValueError:
|
| 400 |
+
return False
|
| 401 |
+
return True
|
| 402 |
+
|
| 403 |
+
|
| 404 |
def execute_features(ctx:RunContextWrapper,task:Literal['churn','stockout','defect'],names:list[str]|None=None,summary:str='')->str:
|
| 405 |
"""Execute proposed features after the system obtains user approval. Empty names means all accepted proposals. summary: Korean status."""
|
| 406 |
def run():
|
| 407 |
+
s=ctx.context;programs=_execution_programs(s,task,names)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
frame,y,subjects,sm=tables(task)
|
| 409 |
fps=[FeatureProgram.from_dict(p) for p in programs]
|
| 410 |
for fp in fps:
|
|
|
|
| 447 |
prob=tabfm(X[:len(y)],y,X[len(y):])
|
| 448 |
rows=sorted([{'row':i,'subject':subject,'probability':float(p)} for i,(subject,p) in enumerate(zip(f['subjects'],prob))],key=lambda r:-r['probability'])
|
| 449 |
run_id=secrets.token_hex(16)
|
| 450 |
+
if not rows:raise ValueError('Prediction did not return any target rows')
|
| 451 |
result={'run_id':run_id,'task':f['task'],'features':[p['name'] for p in f['programs']],'programs':f['programs'],'predictions':rows,'validation':{'method':'stratified synthetic holdout, 72 train / 24 validation','auc':auc,'constant_baseline_auc':0.5,'baseline_probability':baseline},'seconds':round(time.monotonic()-started,3),'model':{'repo':'jingang/TabICL','checkpoint':CHECKPOINT,'output':'real_inference'},'columns':columns(f['task']),'caveat':'Synthetic validation only; target outcomes unknown. No causal interpretation or production accuracy claim.'}
|
| 452 |
+
# Attach the actual evidence before observe() emits the prediction and
|
| 453 |
+
# before the run is stored for publish_results(). The return value,
|
| 454 |
+
# result card and stored report must describe the same observation.
|
| 455 |
+
result['top_evidence']=_feature_evidence(f,rows[0]['row'])
|
| 456 |
ctx.context.runs[run_id]=dict(result,feat_id=feat_id);return result
|
| 457 |
out=observe(ctx,'predict',run);result=json.loads(out)
|
| 458 |
if 'run_id' not in result:return out
|
| 459 |
+
# Preserve predict -> explain ordering and one explain event per row.
|
| 460 |
+
explain_once(ctx,result['run_id'],result['predictions'][0]['row'])
|
| 461 |
+
return out
|
| 462 |
|
| 463 |
|
| 464 |
def explain_once(ctx,run_id,row):
|
| 465 |
"""Emit evidence for a (run, row) once; a repeated request (e.g. the LLM after the automatic one) returns it silently."""
|
| 466 |
seen=ctx.context.__dict__.setdefault('explained',{})
|
| 467 |
if (run_id,row) in seen:return json.dumps(seen[run_id,row],ensure_ascii=False)
|
| 468 |
+
stored=ctx.context.runs.get(run_id,{})
|
| 469 |
+
top=stored.get('top_evidence')
|
| 470 |
+
out=observe(ctx,'explain',lambda:top if isinstance(top,dict) and top.get('row')==row else evidence(ctx.context,run_id,row));data=json.loads(out)
|
| 471 |
if 'error' not in data:seen[run_id,row]=data
|
| 472 |
return out
|
| 473 |
|
| 474 |
|
| 475 |
+
def _feature_evidence(f,row):
|
| 476 |
+
"""Observed source inputs and computed features, without attribution claims."""
|
| 477 |
+
if row<0 or row>=len(f['subjects']):raise ValueError('Unknown target row')
|
| 478 |
+
target=DATA[f['task']]['sheets'][1]
|
| 479 |
+
names=columns(f['task'])
|
| 480 |
+
used={code for p in f['programs'] for code in re.findall(r'\bc[0-3]\b',p['expr'])}
|
| 481 |
+
inputs=[]
|
| 482 |
+
for i,(code,name) in enumerate(names.items()):
|
| 483 |
+
if code not in used:continue
|
| 484 |
+
values=[float(source[i+1]) for source in target['rows']]
|
| 485 |
+
value=values[row]
|
| 486 |
+
inputs.append({'column_code':code,'column':name,'value':value,'unit':None,
|
| 487 |
+
'target_rank_ascending':1+sum(other<value for other in values),
|
| 488 |
+
'target_tie_count':sum(other==value for other in values),
|
| 489 |
+
'target_count':len(values),'target_min':min(values),'target_max':max(values)})
|
| 490 |
+
features=[]
|
| 491 |
+
for i,p in enumerate(f['programs']):
|
| 492 |
+
refs=set(re.findall(r'\bc[0-3]\b',p['expr']))
|
| 493 |
+
features.append({'feature':p['name'],'expr':p['expr'],
|
| 494 |
+
'display_expr':re.sub(r'\bc[0-3]\b',lambda match:names[match.group()],p['expr']),
|
| 495 |
+
'rationale':p['rationale'],'value':float(f['matrix'][len(f['labels'])+row,i]),
|
| 496 |
+
'input_columns':[name for code,name in names.items() if code in refs],'unit':None})
|
| 497 |
+
return {'subject':f['subjects'][row],'row':row,'task':f['task'],'source_sheet':target['name'],
|
| 498 |
+
'input_values':inputs,'features':features,
|
| 499 |
+
'unit_description':'Dimensionless synthetic relative values; no physical units are defined.',
|
| 500 |
+
'caveat':'Observed source inputs and computed feature values, not causal effects or feature importance. '
|
| 501 |
+
'Risk probability does not establish whether an input is high or low; compare actual observed values before making that claim.'}
|
| 502 |
+
|
| 503 |
+
|
| 504 |
def evidence(s,run_id,row):
|
| 505 |
r=s.runs.get(run_id)
|
| 506 |
if not r:raise ValueError('Unknown prediction run in this session')
|
| 507 |
+
return _feature_evidence(s.features[r['feat_id']],row)
|
|
|
|
|
|
|
| 508 |
|
| 509 |
|
| 510 |
def explain(ctx:RunContextWrapper,run_id:str,row:int=0,summary:str='')->str:
|
|
|
|
| 524 |
return {'run_id':run_id,'dashboard_url':'/demo#report='+run_id,'n_rows':len(result['predictions']),'storage':'public synthetic result database, expires after 24 hours or backend restart'}
|
| 525 |
return observe(ctx,'publish_results',run)
|
| 526 |
|
| 527 |
+
def _execution_tool():
|
| 528 |
+
from agents import FunctionTool
|
| 529 |
+
decorated=function_tool(execute_features,strict_mode=False)
|
| 530 |
+
async def invoke(ctx,arguments):
|
| 531 |
+
return await decorated.on_invoke_tool(ctx,arguments)
|
| 532 |
+
# SDK 0.22.3 forces manual approval before a decorated policy whenever
|
| 533 |
+
# defaults/order change during argument validation. A public FunctionTool
|
| 534 |
+
# policy inspects the raw task/names; invocation retains the decorator's
|
| 535 |
+
# complete argument validation and the body's second scope check.
|
| 536 |
+
return FunctionTool(name=decorated.name,description=decorated.description,
|
| 537 |
+
params_json_schema=decorated.params_json_schema,
|
| 538 |
+
strict_json_schema=decorated.strict_json_schema,
|
| 539 |
+
on_invoke_tool=invoke,needs_approval=execution_needs_approval)
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
TOOLS=[_execution_tool() if fn is execute_features else function_tool(fn,strict_mode=False) for fn in [describe_data,query_ontology,propose_features,execute_features,predict,explain,publish_results,show_view]]
|
| 543 |
DEPLOYMENT_NOTES='''
|
| 544 |
PUBLIC DATA ADAPTER (same responsibilities, different data source):
|
| 545 |
Available tasks are churn (๊ณ ๊ฐ ์ดํ), stockout (์ฌ๊ณ ๋ถ์กฑ), defect (์์ฐ ๋ถ๋); describe_data(task="all") lists actual downloaded workbook columns.
|
|
|
|
| 547 |
These three public datasets are synthetic snapshots, NOT the internal rel-f1 database. There is no Superset MCP in this deployment; publish_results writes the actual result database and provides a report URL. Do not claim a Superset dashboard, production evaluation or official test split. predict supplies synthetic holdout AUROC and a constant baseline, with unknown target outcomes. Features have no physical units.
|
| 548 |
When the user asks for a picture, diagram, chart, graph, distribution or schema, call show_view and add at most two sentences; never draw ASCII boxes, tables in code blocks or text diagrams. Never show tool names, function-call syntax or parameters to the user, and never describe a view or capability that show_view does not have; if nothing fits, say so plainly in Korean.
|
| 549 |
For a prediction, keep the original seven-step workflow and execute_features approval pause. For questions or explanations, use data/ontology tools and answer directly without triggering a prediction. Name risk probabilities only from predict output. Explain input evidence and caveats using explain; predict output already includes top_evidence for the highest-risk row. Do not estimate probabilities yourself.
|
| 550 |
+
Always finish the workflow with publish_results; the screen turns predict, explain and publish_results observations into cards (ranking, top probability, evidence, report link). After publishing, answer in two or three Korean sentences using an observable fact from top_evidence and a cautious next check. Evidence is observed input, not feature importance or the reason for the probability. Never call inputs high or low just because predicted risk is high: use the supplied actual value and target rank/range, or omit the comparison. Only the highest-risk row has automatic evidence; do not claim other subjects share its values without querying their evidence. For arithmetic features, keep the actual expression's numerator and denominator. Program rationale is an unvalidated model proposal, not empirical evidence. Use the host's definitions and actual feature expression for arithmetic facts. Do not invent causal effects or model contributions. Read entity counts and distinctness from the tool results. Do not repeat the probability list or AUROC, do not print report URLs, and refer to inputs by their Korean column names, not c0..c3 or snake_case feature names.
|
| 551 |
'''
|
| 552 |
|
| 553 |
|
| 554 |
+
DEPLOYMENT_NOTES+='''
|
| 555 |
+
ํํ ๊ฒ์ฆ: ์ค์ execute_features ๊ด์ธก์ด ๋์ค๊ธฐ ์ ์๋ ์น์ธ ์๋ฃ๋ ์คํ ์๋ฃ๋ผ๊ณ ๋งํ์ง ๋ง์ธ์. ์น์ธ ์นด๋๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ์ํ๋ ์น์ธ ๋๊ธฐ์
๋๋ค.
|
| 556 |
+
ํ์ฌ ๋ฐ์ดํฐ์ ์๋ ๋งค์ถยท์ค๋ช
ยท๋ ์งยท์ธ๋ํคยท์๊ฐ ์ด๋ ฅ ๋ถ์์ ๊ฐ๋ฅํ ์์๋ก ์ ์ํ์ง ๋ง์ธ์. ์์ฒญํ ์ปฌ๋ผ์ด ์์ผ๋ฉด ์ค์ ์ปฌ๋ผ์ ์๋ดํ๊ณ ๋ป์ ํ์ธํ๋ฉฐ, ์กฐ์ฉํ ๋ค๋ฅธ ์ปฌ๋ผ์ผ๋ก ๋ฐ๊พธ๊ฑฐ๋ ์ผ๋ถ๋ง ์ฌ์ฉํ๋ ๊ฒ์ผ๋ก ๋จ์ ํ์ง ๋ง์ธ์.
|
| 557 |
+
์ฐจํธ ์ค๋ช
์ ์ค์ ํ์๋ ์ข
๋ฅ์ ์์น๋ฅผ ๊ธฐ์ค์ผ๋ก ํ์ธ์. ๋ชจ๋ ๊ตฌ๊ฐ์ ๋น๊ตํ์ง ์๊ณ ๋จ์กฐ ์ฆ๊ฐยท๊ฐ์๋ผ๊ณ ์ผ๋ฐํํ์ง ๋ง์ธ์. ํฉ์ฑ ์ค๋
์ท ๊ฐ์ ์
์ฆ๋์ง ์์ ๋ฌผ๋ฆฌ์ ์๋ฏธ๋ ์ธ๊ณผ ํจ๊ณผ๋ฅผ ๋ถ์ด์ง ๋ง์ธ์. ์์ฐ์ค๋ฌ์ด ํ๊ตญ์ด๋ง ์ฌ์ฉํ์ธ์.
|
| 558 |
+
'''
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
def public_instructions():
|
| 562 |
+
"""Use the original tool roles with the public adapter's metrics and style."""
|
| 563 |
+
adapted={
|
| 564 |
+
'4. predict':'4. predict โ actual TabICL probabilities and synthetic holdout metrics from the tool; target outcomes are unknown.',
|
| 565 |
+
'5. explain':'5. explain โ actual observed inputs and feature calculations for the requested target, without causal or importance claims.',
|
| 566 |
+
'6. publish_results':'6. publish_results โ save the actual results and evidence and provide the report link to the screen. Always call this tool to complete a prediction.',
|
| 567 |
+
'7. Answer':'7. Answer in Korean. After prediction, use two or three concise sentences about observed input and a cautious next check. The screen already shows probability ranking, metrics and the report link; do not repeat them or output a URL. For column questions use the actual Korean names, never internal c0/c1/c2/c3 aliases.',
|
| 568 |
+
}
|
| 569 |
+
lines=['You are ONTIC, a data agent for the three published synthetic snapshot workbooks. The ontology constrains available tables and columns. You answer questions, synthesize feature programs and choose tools; the executor computes approved feature expressions and an anonymous matrix goes to TabICL for actual in-context numerical prediction. Do not train model weights or claim access to an internal relational customer database.'];ontology_shape=False;workflow=False
|
| 570 |
+
for line in SYSTEM_PROMPT.splitlines():
|
| 571 |
+
if not workflow:
|
| 572 |
+
if not line.startswith('Workflow for a prediction question:'):continue
|
| 573 |
+
workflow=True
|
| 574 |
+
if line.startswith('Superset tools ('):continue
|
| 575 |
+
if line.startswith('Ontology shape:'):
|
| 576 |
+
ontology_shape=True
|
| 577 |
+
lines.append('Public ontology: tables are owl:Class d:churn, d:stockout, d:defect with rdfs:label. Columns d:<task>.column0..column5 have rdfs:domain, rdfs:label, rdfs:range and ontic:sensitivity. No foreign keys, temporal relations, primaryKey, nullRate, distinct or Metric predicates are published. Use FILTER(?col = d:churn.column0) to restrict the row-key column.')
|
| 578 |
+
continue
|
| 579 |
+
if ontology_shape:
|
| 580 |
+
if not line.startswith('Write the `summary`'):continue
|
| 581 |
+
ontology_shape=False
|
| 582 |
+
lines.append(next((replacement for prefix,replacement in adapted.items() if line.startswith(prefix)),line))
|
| 583 |
+
return '\n'.join(lines)+'\n'+DEPLOYMENT_NOTES
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
def live_public_instructions(ctx,agent):
|
| 587 |
+
"""SDK resumes its stored agent; resolve current user scope on every call."""
|
| 588 |
+
return ctx.context.current_instructions()
|
| 589 |
+
|
| 590 |
+
|
| 591 |
class PublicSession(SessionRuntime):
|
| 592 |
steps=STEPS
|
| 593 |
own_tools=set(STEPS)
|
| 594 |
def __init__(self):
|
| 595 |
+
super().__init__();self.request_text='';self.proposals={};self.proposal_task=None;self.features={};self.runs={};self.touched=time.monotonic()
|
| 596 |
def send(self,text):
|
| 597 |
self.request_text=text
|
| 598 |
return super().send(text)
|
| 599 |
+
def confirm(self,approve,reason=''):
|
| 600 |
+
if not approve and reason.strip():
|
| 601 |
+
self.request_text=reason
|
| 602 |
+
task=self.proposal_task
|
| 603 |
+
if task in self.proposals:
|
| 604 |
+
try:policy=column_policy(task,reason,self.proposals[task]['programs'])
|
| 605 |
+
except ValueError:
|
| 606 |
+
reason+='\n[์
๋ ฅ ๋ฒ์ ๊ฒ์ฆ] ์ด์ ์ ์ ์ํ ์
๋ ฅ ์ค ํ์ฉ๋ ์
๋ ฅ์ด ๋จ์ง ์์์ต๋๋ค. ์ ์
๋ ฅ์ ํ์ธํ๊ธฐ ์ ์๋ ์คํํ์ง ๋ง์ธ์.'
|
| 607 |
+
else:
|
| 608 |
+
if policy['allowed'] is not None:
|
| 609 |
+
allowed=', '.join(columns(task)[c] for c in sorted(policy['allowed']))
|
| 610 |
+
reason+='\n[์
๋ ฅ ๋ฒ์ ๊ฒ์ฆ] ํ์ฌ '+task+'์ ํ์ฉ๋ ์
๋ ฅ์ '+allowed+'๋ฟ์
๋๋ค. ๋๋จธ์ง๋ ์ง์ ์ ์์์ ์ ์ธ ํ ๋จ์ ์
๋ ฅ์ด๋ฉฐ ๋ค๋ฅธ ๋ฐ์ดํฐ ์ปฌ๋ผ์ ์๋ก ํ์ฉํ ๋ป์ด ์๋๋๋ค. ํ ์
๋ ฅ๋ง ๋จ์ผ๋ฉด ์๊ฐ๋ง ์ฌ์ฉํด๋ ๋ฉ๋๋ค.'
|
| 611 |
+
return super().confirm(approve,reason)
|
| 612 |
+
def pending(self):
|
| 613 |
+
actions=super().pending()
|
| 614 |
+
for action in actions:
|
| 615 |
+
if action['tool']=='execute_features':
|
| 616 |
+
task=action['args'].get('task');programs=_execution_programs(self,task,action['args'].get('names'))
|
| 617 |
+
action['programs']=programs;action['columns']=columns(task)
|
| 618 |
+
return actions
|
| 619 |
+
def current_instructions(self):
|
| 620 |
+
instructions=public_instructions()
|
| 621 |
+
task=self.proposal_task
|
| 622 |
+
if task in self.proposals:
|
| 623 |
+
# The same user boundary enforced by tools must also be visible to
|
| 624 |
+
# the deciding agent, before it rewrites a relative edit as a request.
|
| 625 |
+
try:policy=column_policy(task,self.request_text,self.proposals[task]['programs'])
|
| 626 |
+
except ValueError:
|
| 627 |
+
instructions+='\nCURRENT USER INPUT BOUNDARY: No requested inputs remain for '+task+'. Ask the user to choose inputs before proposing or executing.'
|
| 628 |
+
else:
|
| 629 |
+
if policy['allowed'] is not None:
|
| 630 |
+
allowed=', '.join(columns(task)[c] for c in sorted(policy['allowed']))
|
| 631 |
+
instructions+='\nCURRENT USER INPUT BOUNDARY for '+task+': '+allowed+' only. This comes from the latest user request and the previous proposal. Relative edits such as ๋๋จธ์ง retain the remaining previously proposed inputs; they do not authorize other dataset columns. Follow it when proposing or selecting programs. Choose whether to answer, propose or execute according to the user request; execution still requires fresh approval.'
|
| 632 |
+
return instructions
|
| 633 |
def _agent(self,mcp_servers=()):
|
| 634 |
base,key=credentials(self.id)
|
| 635 |
+
return Agent(name='ONTIC',instructions=live_public_instructions,tools=TOOLS,model_settings=ModelSettings(temperature=0,parallel_tool_calls=False),model=OpenAIChatCompletionsModel(model=MODEL,openai_client=AsyncOpenAI(base_url=base,api_key=key,timeout=120,max_retries=0)))
|
| 636 |
|
| 637 |
SESSIONS={}
|
| 638 |
SESSIONS_LOCK=threading.Lock()
|
|
|
|
| 704 |
|
| 705 |
local_router=APIRouter()
|
| 706 |
|
| 707 |
+
|
| 708 |
+
def visible_answer(text):
|
| 709 |
+
"""Remove Qwen's reasoning protocol from its final answer, before streaming."""
|
| 710 |
+
if not isinstance(text,str):return text
|
| 711 |
+
text=re.sub(r'<think\b[^>]*>.*?</think\s*>','',text,flags=re.I|re.S)
|
| 712 |
+
# Some OpenAI-compatible responses omit the opening tag but retain its close.
|
| 713 |
+
closes=list(re.finditer(r'</think\s*>',text,re.I))
|
| 714 |
+
if closes:text=text[closes[-1].end():]
|
| 715 |
+
text=re.split(r'<think\b[^>]*>',text,maxsplit=1,flags=re.I)[0]
|
| 716 |
+
return text.strip()
|
| 717 |
+
|
| 718 |
+
|
| 719 |
+
def visible_completion(result):
|
| 720 |
+
"""Keep final text and tool calls; reasoning never enters the public stream."""
|
| 721 |
+
choices=result.get('choices')
|
| 722 |
+
if not isinstance(choices,list) or not choices:raise ValueError('The model returned no completion choices')
|
| 723 |
+
out=dict(result);out['choices']=[]
|
| 724 |
+
for choice in choices:
|
| 725 |
+
if not isinstance(choice,dict) or not isinstance(choice.get('message'),dict):raise ValueError('The model returned no message')
|
| 726 |
+
message=dict(choice['message'])
|
| 727 |
+
for key in ('reasoning','reasoning_content','thinking'):message.pop(key,None)
|
| 728 |
+
if isinstance(message.get('content'),str):message['content']=visible_answer(message['content'])
|
| 729 |
+
if not message.get('content') and not message.get('tool_calls'):
|
| 730 |
+
raise ValueError('The model did not return a complete answer or tool call')
|
| 731 |
+
out['choices'].append(dict(choice,message=message))
|
| 732 |
+
return out
|
| 733 |
+
|
| 734 |
+
|
| 735 |
@local_router.post('/local-llm/v1/chat/completions')
|
| 736 |
async def local_completion(request:Request):
|
| 737 |
if not request.client or request.client.host not in ('127.0.0.1','::1'):raise HTTPException(403,'Loopback only')
|
|
|
|
| 743 |
try:result=await asyncio.to_thread(dispatch,'llm',dict(data,model=MODEL,stream=False),strict=True)
|
| 744 |
except GPUQwenUnavailable as exc:
|
| 745 |
return JSONResponse({'error':{'message':str(exc),'type':'gpu_qwen_unavailable','code':'GPU_QWEN_UNAVAILABLE','reason':exc.reason}},status_code=503)
|
| 746 |
+
try:result=visible_completion(result)
|
| 747 |
+
except ValueError:
|
| 748 |
+
return JSONResponse({'error':{'message':'๋ชจ๋ธ์ด ์์ฑ๋ ๋ต๋ณ์ ๋ฐํํ์ง ๋ชปํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.','code':'INCOMPLETE_MODEL_OUTPUT'}},status_code=502)
|
| 749 |
if not data.get('stream'):return result
|
| 750 |
choice=result['choices'][0];message=choice['message'];delta=dict(message)
|
| 751 |
if message.get('tool_calls'):delta['tool_calls']=[dict(t,index=i) for i,t in enumerate(message['tool_calls'])]
|
requirements.txt
CHANGED
|
@@ -5,10 +5,11 @@ tabicl[forecast]==2.2.0
|
|
| 5 |
torch==2.14.0
|
| 6 |
uvicorn==0.52.4
|
| 7 |
|
| 8 |
-
openai-agents
|
| 9 |
-
openai
|
| 10 |
duckdb>=1.5.5,<2
|
| 11 |
pandas>=2.2,<3
|
| 12 |
pyyaml>=6,<7
|
| 13 |
-
rdflib
|
|
|
|
| 14 |
cryptography>=42,<49
|
|
|
|
| 5 |
torch==2.14.0
|
| 6 |
uvicorn==0.52.4
|
| 7 |
|
| 8 |
+
openai-agents==0.22.3
|
| 9 |
+
openai==3.16.2
|
| 10 |
duckdb>=1.5.5,<2
|
| 11 |
pandas>=2.2,<3
|
| 12 |
pyyaml>=6,<7
|
| 13 |
+
rdflib==7.6.0
|
| 14 |
+
pyparsing==3.3.3
|
| 15 |
cryptography>=42,<49
|
web/agent-ui.js
CHANGED
|
@@ -11,6 +11,7 @@ export function agentSurface(id,event){
|
|
| 11 |
else if(event.type==='approval'){
|
| 12 |
const pending=event.actions?.[0];if(pending?.tool!=='execute_features')throw new Error('์น์ธํ ๋๊ตฌ๋ฅผ ํ์ธํ์ง ๋ชปํ์ต๋๋ค.');
|
| 13 |
items=[text('title','์ด ๊ธฐ์ค์ผ๋ก ์ํ๋๋ฅผ ๊ณ์ฐํ ๊น์?','heading'),text('scope','ํ์ต ๋ฐ์ดํฐ 96ํ์ ๋ฌธ๋งฅ์ผ๋ก TabICL์ด ๋ฐ๋ก ๊ณ์ฐํฉ๋๋ค(๋ชจ๋ธ ํ์ต ์์). ๋นผ๊ฑฐ๋ ๋ฐ๊ฟ ๊ธฐ์ค์ ์ฑํ
์ผ๋ก ์๋ ค ์ฃผ์ธ์.','muted'),{id:'actions',component:'Actions',buttons:[{id:'approve',label:'์ด๋๋ก ์์ธก',primary:true,action:{event:{name:'approve_prediction'}}},{id:'edit',label:'๊ธฐ์ค ๋ฐ๊พธ๊ธฐ',primary:false,action:{event:{name:'edit_question'}}}]}];
|
|
|
|
| 14 |
model={selected_features:[0]};
|
| 15 |
}else if(event.tool==='describe_data'){
|
| 16 |
if(!Array.isArray(d.datasets))throw new Error('๋ฐ์ดํฐ ์ฌ์ ์๋ต์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.');
|
|
@@ -29,18 +30,23 @@ export function agentSurface(id,event){
|
|
| 29 |
items=[table('ontology','์จํจ๋ก์ง ์กฐํ',keys,rows.map(r=>keys.map(k=>r[k]??'')))];
|
| 30 |
}else if(event.tool==='propose_features'){
|
| 31 |
if(!Array.isArray(d.programs))throw new Error('ํผ์ฒ ์ ์ ์๋ต์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.');
|
| 32 |
-
items=[text('label','์์ธก์ ์ธ ๊ธฐ์ค','label'),text('programs',d.programs.map(p=>`- **${korean(p.expr,d.columns)}** โ ${p.
|
| 33 |
if(d.rejected?.length)items.push(text('rejected',`์ง๋ฌธ๊ณผ ๋ง์ง ์๊ฑฐ๋ ๊ฒ์ฆ์ ํต๊ณผํ์ง ๋ชปํ ์ ์ ${d.rejected.length}๊ฐ๋ ๋บ์ต๋๋ค.`,'muted'));
|
| 34 |
}else if(event.tool==='execute_features')return [];
|
| 35 |
else if(event.tool==='predict'){
|
| 36 |
if(d.model?.output!=='real_inference'||!Array.isArray(d.predictions)||d.predictions.some(r=>!Number.isFinite(r.probability)||r.probability<0||r.probability>1))throw new Error('์ค์ ์์ธก ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ์ง ๋ชปํ์ต๋๋ค.');
|
| 37 |
const ui=SCENARIO_UI[d.task]||{name:'์์ธก',title:'์ํ ์์',next:''};const top=d.predictions[0];
|
| 38 |
-
const
|
|
|
|
| 39 |
items=[text('label',`${ui.name} ์์ธก ยท ํฉ์ฑ ๋ฐ์ดํฐ`,'label'),text('answer',`${top.subject}๋ถํฐ ํ์ธํ์ธ์.`,'heading'),{id:'metric',component:'Metric',label:`${top.subject}์ ์์ธก ํ๋ฅ `,value:{path:'/top'}},{id:'ranking',component:'Ranking',title:ui.title,items:{path:'/predictions'}}];
|
| 40 |
if(ui.next)items.push(text('next',ui.next,'callout'));
|
| 41 |
items.push({id:'evidence',component:'Evidence',title:`๊ทผ๊ฑฐ์ ๋ชจ๋ธ ๋ณด๊ธฐ ยท ${top.subject} ์
๋ ฅ๊ฐ`,features:{path:'/features'},signals:{path:'/signals'},model:{path:'/model'},timing:{path:'/timing'}});
|
| 42 |
model={top:top.probability,predictions:d.predictions,features:d.programs.map(p=>korean(p.expr,d.columns)),signals,model:d.model,timing:`๋ณด๋ฅ ๊ฒ์ฆ AUROC ${d.validation.auc.toFixed(3)} (์์ ๊ธฐ์ค์ 0.5) ยท ๊ณ์ฐ ${d.seconds}์ด`};
|
| 43 |
-
}else if(event.tool==='explain')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
else if(event.tool==='publish_results'){
|
| 45 |
if(!/^\/demo#report=[0-9a-f]{32}$/.test(d.dashboard_url))throw new Error('๊ฒฐ๊ณผ ๋งํฌ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.');
|
| 46 |
items=[{id:'report',component:'ReportLink',label:'๊ณต์ ์ฉ ๊ฒฐ๊ณผ ํ์ด์ง ์ด๊ธฐ โ',url:d.dashboard_url}];
|
|
|
|
| 11 |
else if(event.type==='approval'){
|
| 12 |
const pending=event.actions?.[0];if(pending?.tool!=='execute_features')throw new Error('์น์ธํ ๋๊ตฌ๋ฅผ ํ์ธํ์ง ๋ชปํ์ต๋๋ค.');
|
| 13 |
items=[text('title','์ด ๊ธฐ์ค์ผ๋ก ์ํ๋๋ฅผ ๊ณ์ฐํ ๊น์?','heading'),text('scope','ํ์ต ๋ฐ์ดํฐ 96ํ์ ๋ฌธ๋งฅ์ผ๋ก TabICL์ด ๋ฐ๋ก ๊ณ์ฐํฉ๋๋ค(๋ชจ๋ธ ํ์ต ์์). ๋นผ๊ฑฐ๋ ๋ฐ๊ฟ ๊ธฐ์ค์ ์ฑํ
์ผ๋ก ์๋ ค ์ฃผ์ธ์.','muted'),{id:'actions',component:'Actions',buttons:[{id:'approve',label:'์ด๋๋ก ์์ธก',primary:true,action:{event:{name:'approve_prediction'}}},{id:'edit',label:'๊ธฐ์ค ๋ฐ๊พธ๊ธฐ',primary:false,action:{event:{name:'edit_question'}}}]}];
|
| 14 |
+
if(pending.programs?.length)items.splice(1,0,text('selected','์คํํ ๊ธฐ์ค: '+pending.programs.map(p=>korean(p.expr,pending.columns)).join(' ยท ')));
|
| 15 |
model={selected_features:[0]};
|
| 16 |
}else if(event.tool==='describe_data'){
|
| 17 |
if(!Array.isArray(d.datasets))throw new Error('๋ฐ์ดํฐ ์ฌ์ ์๋ต์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.');
|
|
|
|
| 30 |
items=[table('ontology','์จํจ๋ก์ง ์กฐํ',keys,rows.map(r=>keys.map(k=>r[k]??'')))];
|
| 31 |
}else if(event.tool==='propose_features'){
|
| 32 |
if(!Array.isArray(d.programs))throw new Error('ํผ์ฒ ์ ์ ์๋ต์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.');
|
| 33 |
+
items=[text('label','์์ธก์ ์ธ ๊ธฐ์ค','label'),text('programs',d.programs.map(p=>`- **${korean(p.expr,d.columns)}**${d.definitions?.[p.name]?` โ ${d.definitions[p.name]}`:''}`).join('\n'))];
|
| 34 |
if(d.rejected?.length)items.push(text('rejected',`์ง๋ฌธ๊ณผ ๋ง์ง ์๊ฑฐ๋ ๊ฒ์ฆ์ ํต๊ณผํ์ง ๋ชปํ ์ ์ ${d.rejected.length}๊ฐ๋ ๋บ์ต๋๋ค.`,'muted'));
|
| 35 |
}else if(event.tool==='execute_features')return [];
|
| 36 |
else if(event.tool==='predict'){
|
| 37 |
if(d.model?.output!=='real_inference'||!Array.isArray(d.predictions)||d.predictions.some(r=>!Number.isFinite(r.probability)||r.probability<0||r.probability>1))throw new Error('์ค์ ์์ธก ๊ฒฐ๊ณผ๋ฅผ ํ์ธํ์ง ๋ชปํ์ต๋๋ค.');
|
| 38 |
const ui=SCENARIO_UI[d.task]||{name:'์์ธก',title:'์ํ ์์',next:''};const top=d.predictions[0];
|
| 39 |
+
const inputs=d.top_evidence?.input_values||[];
|
| 40 |
+
const signals=[...inputs.map(v=>({feature:v.column,value:v.value})),...(d.top_evidence?.features||[]).filter(f=>!inputs.some(v=>f.expr===v.column_code)).map(f=>({feature:f.display_expr||korean(f.expr,d.columns),value:f.value}))];
|
| 41 |
items=[text('label',`${ui.name} ์์ธก ยท ํฉ์ฑ ๋ฐ์ดํฐ`,'label'),text('answer',`${top.subject}๋ถํฐ ํ์ธํ์ธ์.`,'heading'),{id:'metric',component:'Metric',label:`${top.subject}์ ์์ธก ํ๋ฅ `,value:{path:'/top'}},{id:'ranking',component:'Ranking',title:ui.title,items:{path:'/predictions'}}];
|
| 42 |
if(ui.next)items.push(text('next',ui.next,'callout'));
|
| 43 |
items.push({id:'evidence',component:'Evidence',title:`๊ทผ๊ฑฐ์ ๋ชจ๋ธ ๋ณด๊ธฐ ยท ${top.subject} ์
๋ ฅ๊ฐ`,features:{path:'/features'},signals:{path:'/signals'},model:{path:'/model'},timing:{path:'/timing'}});
|
| 44 |
model={top:top.probability,predictions:d.predictions,features:d.programs.map(p=>korean(p.expr,d.columns)),signals,model:d.model,timing:`๋ณด๋ฅ ๊ฒ์ฆ AUROC ${d.validation.auc.toFixed(3)} (์์ ๊ธฐ์ค์ 0.5) ยท ๊ณ์ฐ ${d.seconds}์ด`};
|
| 45 |
+
}else if(event.tool==='explain'){
|
| 46 |
+
items=[];
|
| 47 |
+
if(d.input_values?.length)items.push(table('source-inputs',`${d.subject}์ ์๋ณธ ์
๋ ฅ`,['์ปฌ๋ผ','์
๋ ฅ๊ฐ'],d.input_values.map(v=>[v.column,Number(v.value).toFixed(2)])));
|
| 48 |
+
items.push(table('evidence',`${d.subject}์ ๊ณ์ฐ๋ ๊ธฐ์ค๊ฐ`,['๊ธฐ์ค','๊ณ์ฐ๊ฐ'],(d.features||[]).map(f=>[f.display_expr||korean(f.expr,d.columns),Number(f.value).toFixed(2)])),text('caveat','๊ด์ธก ์
๋ ฅ๊ฐ์ด๋ฉฐ ์ธ๊ณผ๊ด๊ณ๋ ๋ณ์ ์ค์๋๋ฅผ ๋ปํ์ง ์์ต๋๋ค.','muted'));
|
| 49 |
+
}
|
| 50 |
else if(event.tool==='publish_results'){
|
| 51 |
if(!/^\/demo#report=[0-9a-f]{32}$/.test(d.dashboard_url))throw new Error('๊ฒฐ๊ณผ ๋งํฌ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.');
|
| 52 |
items=[{id:'report',component:'ReportLink',label:'๊ณต์ ์ฉ ๊ฒฐ๊ณผ ํ์ด์ง ์ด๊ธฐ โ',url:d.dashboard_url}];
|
web/demo-chat.js
CHANGED
|
@@ -13,14 +13,14 @@ function lock(busy){form.toggleAttribute('aria-busy',busy);form.querySelector('b
|
|
| 13 |
function newChat(){generation++;active?.abort();active=null;sid=null;turns.forEach(t=>t.renderer.setLocked(true));turns.length=0;conversation.replaceChildren();$('#welcome').hidden=false;question.value='';history.replaceState(null,'','#start');show('start');lock(false);question.focus();announce('์ ๋ํ๋ฅผ ์์ํ์ต๋๋ค.');}
|
| 14 |
$('#new-chat').onclick=newChat;$('#new-chat-mobile').onclick=newChat;
|
| 15 |
function makeTurn(text){$('#welcome').hidden=true;show('start');const article=node('article','chat-turn');if(text)article.append(node('p','user-message',text));const label=node('div','assistant-label');label.append(node('b','','O'),node('span','','ONTIC'));const host=node('div','assistant-content');article.append(label,host);conversation.append(article);const turn={node:article,host,text,renderer:new A2UIRenderer(host,event=>action(turn,event)),pending:false,answer:null};turns.push(turn);article.scrollIntoView({behavior:'smooth',block:'start'});return turn;}
|
| 16 |
-
function render(turn,event,id){const messages=agentSurface(id,event);messages.forEach(m=>turn.renderer.consume(m));}
|
| 17 |
function action(turn,event){if(active||turn!==turns.at(-1)||!turn.pending)return;
|
| 18 |
if(event.action.name==='approve_prediction'){turn.pending=false;turn.renderer.setLocked(true);run({sid,approve:true},turn);}
|
| 19 |
else{question.focus();question.placeholder='์ ์ธํ ํผ์ฒ๋ ๋ฐ๊ฟ ์กฐ๊ฑด์ ์๋ ค ์ฃผ์ธ์';announce('๋ฐ๊ฟ ์กฐ๊ฑด์ ์
๋ ฅํ๋ฉด ์์ด์ ํธ๊ฐ ์ ์์ ์์ ํฉ๋๋ค.');}
|
| 20 |
}
|
| 21 |
function failure(turn,err){const box=node('div','chat-error');box.setAttribute('role','alert');box.append(node('p','',err.message));if(err.request_id)box.append(node('p','fineprint',`์์ฒญ ID: ${err.request_id}`));turn.host.append(box);announce(err.message);}
|
| 22 |
async function run(payload,turn){
|
| 23 |
-
const mine=generation,controller=new AbortController();active=controller;turn.answer=null;lock(true);
|
| 24 |
const statusId='agent-status-'+(++counter);surface(statusId,[{id:'root',component:'Column',children:['status']},{id:'status',component:'Text',tone:'status',text:'๋ฐ์ดํฐ์ ๋ํ ์ด๋ ฅ์ ํ์ธํ๊ณ ์์ด์.'}],{}).forEach(m=>turn.renderer.consume(m));
|
| 25 |
const status=text=>{if(turn.renderer.surfaces.has(statusId))turn.renderer.consume({version:'v0.9',updateComponents:{surfaceId:statusId,components:[{id:'root',component:'Column',children:['status']},{id:'status',component:'Text',tone:'status',text}]}});announce(text);};
|
| 26 |
let timeout;const resetTimer=()=>{clearTimeout(timeout);timeout=setTimeout(()=>controller.abort(),150000);};resetTimer();let terminal=false;
|
|
@@ -34,9 +34,9 @@ async function run(payload,turn){
|
|
| 34 |
else if(e.type==='action'){status(e.summary||'์์ด์ ํธ๊ฐ ๋๊ตฌ๋ฅผ ์คํํ๊ณ ์์ด์.');}
|
| 35 |
else if(e.type==='observation'){
|
| 36 |
if(e.data?.error){status('๋๊ตฌ ์๋ต์ ํ์ธํ๊ณ ์์ ํ๊ณ ์์ด์.');return;}
|
| 37 |
-
if(e.tool==='predict')turn.topRow=e.data?.predictions?.[0]?.row;
|
| 38 |
-
if(e.tool==='explain'&&e.data?.row===turn.topRow)return; //
|
| 39 |
-
const observationId='agent-tool-'+(++counter);render(turn,e,observationId);
|
| 40 |
if(e.tool==='predict'){const n=turn.renderer.surfaces.get(observationId)?.node;if(n)window.scrollTo({top:n.getBoundingClientRect().top+scrollY-80,left:0,behavior:'smooth'});} // the answer, not the approval card, should be on screen
|
| 41 |
if(['describe_data','query_ontology','explain'].includes(e.tool)){const rendered=turn.renderer.surfaces.get(observationId)?.node;if(rendered){const details=node('details','tool-observation');details.append(node('summary','',e.tool==='explain'?'์
๋ ฅ ๊ทผ๊ฑฐ ๋ณด๊ธฐ':'์กฐํํ ๋ฐ์ดํฐยท์ปฌ๋ผ ๋ณด๊ธฐ'));rendered.before(details);details.append(rendered);}}
|
| 42 |
}else if(e.type==='token'){
|
|
|
|
| 13 |
function newChat(){generation++;active?.abort();active=null;sid=null;turns.forEach(t=>t.renderer.setLocked(true));turns.length=0;conversation.replaceChildren();$('#welcome').hidden=false;question.value='';history.replaceState(null,'','#start');show('start');lock(false);question.focus();announce('์ ๋ํ๋ฅผ ์์ํ์ต๋๋ค.');}
|
| 14 |
$('#new-chat').onclick=newChat;$('#new-chat-mobile').onclick=newChat;
|
| 15 |
function makeTurn(text){$('#welcome').hidden=true;show('start');const article=node('article','chat-turn');if(text)article.append(node('p','user-message',text));const label=node('div','assistant-label');label.append(node('b','','O'),node('span','','ONTIC'));const host=node('div','assistant-content');article.append(label,host);conversation.append(article);const turn={node:article,host,text,renderer:new A2UIRenderer(host,event=>action(turn,event)),pending:false,answer:null};turns.push(turn);article.scrollIntoView({behavior:'smooth',block:'start'});return turn;}
|
| 16 |
+
function render(turn,event,id){const messages=agentSurface(id,event);messages.forEach(m=>{if(m.createSurface&&turn.renderer.surfaces.has(id))return;turn.renderer.consume(m);});}
|
| 17 |
function action(turn,event){if(active||turn!==turns.at(-1)||!turn.pending)return;
|
| 18 |
if(event.action.name==='approve_prediction'){turn.pending=false;turn.renderer.setLocked(true);run({sid,approve:true},turn);}
|
| 19 |
else{question.focus();question.placeholder='์ ์ธํ ํผ์ฒ๋ ๋ฐ๊ฟ ์กฐ๊ฑด์ ์๋ ค ์ฃผ์ธ์';announce('๋ฐ๊ฟ ์กฐ๊ฑด์ ์
๋ ฅํ๋ฉด ์์ด์ ํธ๊ฐ ์ ์์ ์์ ํฉ๋๋ค.');}
|
| 20 |
}
|
| 21 |
function failure(turn,err){const box=node('div','chat-error');box.setAttribute('role','alert');box.append(node('p','',err.message));if(err.request_id)box.append(node('p','fineprint',`์์ฒญ ID: ${err.request_id}`));turn.host.append(box);announce(err.message);}
|
| 22 |
async function run(payload,turn){
|
| 23 |
+
const mine=generation,controller=new AbortController();active=controller;turn.answer=null;turn.proposalSurfaceId=null;lock(true);
|
| 24 |
const statusId='agent-status-'+(++counter);surface(statusId,[{id:'root',component:'Column',children:['status']},{id:'status',component:'Text',tone:'status',text:'๋ฐ์ดํฐ์ ๋ํ ์ด๋ ฅ์ ํ์ธํ๊ณ ์์ด์.'}],{}).forEach(m=>turn.renderer.consume(m));
|
| 25 |
const status=text=>{if(turn.renderer.surfaces.has(statusId))turn.renderer.consume({version:'v0.9',updateComponents:{surfaceId:statusId,components:[{id:'root',component:'Column',children:['status']},{id:'status',component:'Text',tone:'status',text}]}});announce(text);};
|
| 26 |
let timeout;const resetTimer=()=>{clearTimeout(timeout);timeout=setTimeout(()=>controller.abort(),150000);};resetTimer();let terminal=false;
|
|
|
|
| 34 |
else if(e.type==='action'){status(e.summary||'์์ด์ ํธ๊ฐ ๋๊ตฌ๋ฅผ ์คํํ๊ณ ์์ด์.');}
|
| 35 |
else if(e.type==='observation'){
|
| 36 |
if(e.data?.error){status('๋๊ตฌ ์๋ต์ ํ์ธํ๊ณ ์์ ํ๊ณ ์์ด์.');return;}
|
| 37 |
+
if(e.tool==='predict'){turn.topRow=e.data?.predictions?.[0]?.row;turn.hasTopEvidence=Boolean(e.data?.top_evidence?.features?.length);}
|
| 38 |
+
if(e.tool==='explain'&&e.data?.row===turn.topRow&&turn.hasTopEvidence)return; // suppress only evidence actually present in the result card
|
| 39 |
+
const observationId=e.tool==='propose_features'?(turn.proposalSurfaceId??=( 'agent-proposal-'+(++counter))):'agent-tool-'+(++counter);render(turn,e,observationId);
|
| 40 |
if(e.tool==='predict'){const n=turn.renderer.surfaces.get(observationId)?.node;if(n)window.scrollTo({top:n.getBoundingClientRect().top+scrollY-80,left:0,behavior:'smooth'});} // the answer, not the approval card, should be on screen
|
| 41 |
if(['describe_data','query_ontology','explain'].includes(e.tool)){const rendered=turn.renderer.surfaces.get(observationId)?.node;if(rendered){const details=node('details','tool-observation');details.append(node('summary','',e.tool==='explain'?'์
๋ ฅ ๊ทผ๊ฑฐ ๋ณด๊ธฐ':'์กฐํํ ๋ฐ์ดํฐยท์ปฌ๋ผ ๋ณด๊ธฐ'));rendered.before(details);details.append(rendered);}}
|
| 42 |
}else if(e.type==='token'){
|