fbdeme commited on
Commit
bbcfa3d
Β·
verified Β·
1 Parent(s): ace0798

Deploy 444e366 from GitHub Actions

Browse files
README.md CHANGED
@@ -12,4 +12,6 @@ ONTIC 곡개 μ œν’ˆ λ―Έλ¦¬λ³΄κΈ°μ—μ„œ μ‚¬μš©ν•˜λŠ” ν•©μ„± 데λͺ¨ 및 μ‚¬μš©μž
12
 
13
  `/predict-upload`λŠ” 읡λͺ… 숫자 ν–‰λ ¬κ³Ό μ •λ‹΅, μ‹œκ³„μ—΄ λ‚ μ§œλ₯Ό λ°›μ•„ μ‹€μ œ TabICL μΆ”λ‘ κ³Ό 보λ₯˜ 검증을 μ‹€ν–‰ν•œλ‹€. μž…λ ₯ 행은 μ €μž₯ν•˜μ§€ μ•ŠλŠ”λ‹€. λΆ„λ₯˜Β·νšŒκ·€λŠ” μ΅œλŒ€ 1,000개 κ³Όκ±° 사둀·30개 λ³€μˆ˜Β·100개 예츑 λŒ€μƒ, μ‹œκ³„μ—΄μ€ μ΅œλŒ€ 1,000개 κ΄€μΈ‘κ³Ό 24개 미래 κΈ°κ°„μœΌλ‘œ μ œν•œν•œλ‹€.
14
 
15
- 곡개 μ±„νŒ…μ€ 졜초 λ‚΄λΆ€ 데λͺ¨μ™€ κ³΅μœ ν•˜λŠ” `SessionRuntime`Β·μ—­ν•  ν”„λ‘¬ν”„νŠΈΒ·ν”Όμ²˜ ν”„λ‘œκ·Έλž¨ 검증기λ₯Ό μ‚¬μš©ν•œλ‹€. Llamaκ°€ μ‘°νšŒΒ·μ œμ•ˆΒ·μ„€λͺ…을 μˆ˜ν–‰ν•˜κ³ , 승인 λ’€ TabICL이 수치 μ˜ˆμΈ‘μ„ μˆ˜ν–‰ν•œλ‹€. `python poc/deploy/build_space.py <좜λ ₯ 디렉터리>`둜 곡톡 μ†ŒμŠ€κΉŒμ§€ ν¬ν•¨ν•œ HF μ—…λ‘œλ“œ 디렉터리λ₯Ό λ§Œλ“ λ‹€. μ›λž˜ 배포 λΉ„λ°€ν‚€λŠ” Workerμ—λ§Œ μœ μ§€ν•œλ‹€. 곡개 데이터 μŠ€λƒ…μƒ·κ³Ό κ²°κ³Ό DB μ–΄λŒ‘ν„°μ˜ μ°¨μ΄λŠ” `docs/12_agent_role_restore_2026-10-02.md`에 κΈ°λ‘ν•œλ‹€.
 
 
 
12
 
13
  `/predict-upload`λŠ” 읡λͺ… 숫자 ν–‰λ ¬κ³Ό μ •λ‹΅, μ‹œκ³„μ—΄ λ‚ μ§œλ₯Ό λ°›μ•„ μ‹€μ œ TabICL μΆ”λ‘ κ³Ό 보λ₯˜ 검증을 μ‹€ν–‰ν•œλ‹€. μž…λ ₯ 행은 μ €μž₯ν•˜μ§€ μ•ŠλŠ”λ‹€. λΆ„λ₯˜Β·νšŒκ·€λŠ” μ΅œλŒ€ 1,000개 κ³Όκ±° 사둀·30개 λ³€μˆ˜Β·100개 예츑 λŒ€μƒ, μ‹œκ³„μ—΄μ€ μ΅œλŒ€ 1,000개 κ΄€μΈ‘κ³Ό 24개 미래 κΈ°κ°„μœΌλ‘œ μ œν•œν•œλ‹€.
14
 
15
+ 곡개 μ±„νŒ…μ€ 졜초 λ‚΄λΆ€ 데λͺ¨μ™€ κ³΅μœ ν•˜λŠ” `SessionRuntime`Β·μ—­ν•  ν”„λ‘¬ν”„νŠΈΒ·ν”Όμ²˜ ν”„λ‘œκ·Έλž¨ 검증기λ₯Ό μ‚¬μš©ν•œλ‹€. GPU μ„œλ²„ Ollama의 `qwen3.8:27b`κ°€ μ‘°νšŒΒ·μ œμ•ˆΒ·λ„κ΅¬ ν˜ΈμΆœΒ·μ„€λͺ…을 μˆ˜ν–‰ν•˜κ³ , 승인 λ’€ TabICL이 수치 μ˜ˆμΈ‘μ„ μˆ˜ν–‰ν•œλ‹€. κΈ°μ‘΄ ν™ˆ μ„œλ²„ runnerκ°€ Space둜 μž‘μ—…μ„ κ°€μ Έκ°€λ―€λ‘œ GPU μ„œλ²„λ₯Ό κ³΅κ°œν•˜μ§€ μ•ŠλŠ”λ‹€. Qwen 연결이 끊기면 λŒ€κΈ°/였λ₯˜λ₯Ό ν‘œμ‹œν•˜λ©° λ‹€λ₯Έ LLM으둜 μžλ™ μ „ν™˜ν•˜μ§€ μ•ŠλŠ”λ‹€. `/demo`μ—μ„œ λŒ€ν™”λ₯Ό μ‹œμž‘ν•˜κ³  `/health`와 `/api/agent-ready`μ—μ„œ μ—°κ²° μƒνƒœλ₯Ό ν™•μΈν•œλ‹€.
16
+
17
+ `python poc/deploy/build_space.py <좜λ ₯ 디렉터리>`둜 곡톡 μ†ŒμŠ€κΉŒμ§€ ν¬ν•¨ν•œ HF μ—…λ‘œλ“œ 디렉터리λ₯Ό λ§Œλ“ λ‹€. 곡개 νŽ˜μ΄μ§€μ™€ APIλŠ” 같은 Spaceμ—μ„œ μ œκ³΅ν•˜λ©°, runner 인증 λΉ„λ°€ν‚€λŠ” ν™˜κ²½λ³€μˆ˜λ‘œλ§Œ κ΄€λ¦¬ν•œλ‹€. 곡개 데이터 μŠ€λƒ…μƒ·κ³Ό κ²°κ³Ό DB μ–΄λŒ‘ν„°μ˜ μ°¨μ΄λŠ” `docs/12_agent_role_restore_2026-10-02.md`에 κΈ°λ‘ν•œλ‹€.
__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
 
app.py CHANGED
@@ -255,7 +255,8 @@ app.add_api_route("/api/predict-upload", predict_upload, methods=["POST"], depen
255
 
256
  @app.get("/api/agent-ready")
257
  def agent_ready() -> dict:
258
- return {"ready": True}
 
259
 
260
 
261
  @app.get("/api/agent-info")
 
255
 
256
  @app.get("/api/agent-ready")
257
  def agent_ready() -> dict:
258
+ if public_agent.qwen_ready():return {"ready": True}
259
+ return JSONResponse({"ready":False,"state":"gpu_offline","message":"GPU μ„œλ²„μ˜ Qwen 연결을 기닀리고 μžˆμŠ΅λ‹ˆλ‹€."},status_code=503)
260
 
261
 
262
  @app.get("/api/agent-info")
public_agent.py CHANGED
@@ -33,14 +33,13 @@ from feature_program import Executor, FeatureProgram, validate
33
  from semantic_model import SemanticModel
34
 
35
  DATA = json.loads(Path(__file__).with_name('public_datasets.json').read_text())
36
- # LLM and TabFM calls go to the home-server runner (GPU box, outbound-only polling) while it polls, else to this fallback.
37
  RUNNER_TOKEN = os.environ.get('ONTIC_RUNNER_TOKEN', '')
38
- FALLBACK = {'base': os.environ.get('ONTIC_FALLBACK_BASE_URL', 'https://openrouter.ai/api/v1').rstrip('/'), 'key': os.environ.get('OPENROUTER_API_KEY', ''), 'model': os.environ.get('ONTIC_FALLBACK_MODEL', 'nvidia/nemotron-3-ultra-550b-a55b:free')}
39
- MODEL = FALLBACK['model']
40
  CHECKPOINT = 'tabicl-classifier-v2-20260212.ckpt'
41
  TABFM_URL = os.environ.get('ONTIC_TABFM_URL', '').rstrip('/')
42
  # What the screen says about the agent comes from here, not from page text (a local run overrides it).
43
- LLM = {'model':MODEL,'name':os.environ.get('ONTIC_FALLBACK_NAME','Nemotron 3 Ultra'),'provider':'OpenRouter'}
44
  STEPS = {'describe_data':1,'show_view':1,'query_ontology':1,'propose_features':2,'execute_features':4,'predict':5,'explain':6,'publish_results':7}
45
  MODEL_LOCK = threading.Lock()
46
  RESULT_LOCK = threading.Lock()
@@ -54,30 +53,41 @@ def runner_alive():
54
  return bool(RUNNER_TOKEN) and time.monotonic() - RUNNER['seen'] < 40
55
 
56
 
 
 
 
 
57
  def current_llm():
58
- return RUNNER['llm'] if runner_alive() and RUNNER['llm'] else LLM
59
 
60
 
61
- def dispatch(kind, payload, claim=15, timeout=180):
62
- """Hand a job to the runner. None when no runner polls, nobody claims it in `claim` s, or no answer in time: the caller falls back."""
63
- if not runner_alive():
 
 
 
 
 
 
 
64
  return None
 
 
 
65
  jid = secrets.token_hex(16); job = {'claimed': threading.Event(), 'ready': threading.Event(), 'result': None}
66
  PENDING[jid] = job; JOBS.put({'id': jid, 'kind': kind, 'payload': payload})
67
  try:
68
- if not job['claimed'].wait(claim) or not job['ready'].wait(timeout):
69
- return None
70
  result = job['result']
71
- return None if not isinstance(result, dict) or 'error' in result else result
 
 
72
  finally:
73
  PENDING.pop(jid, None)
74
 
75
 
76
- def fallback_completion(data):
77
- r = httpx.post(FALLBACK['base'] + '/chat/completions', json=dict(data, model=FALLBACK['model'], stream=False), headers={'authorization': 'Bearer ' + FALLBACK['key']}, timeout=120)
78
- return r.json() if r.status_code == 200 else {'error': f'fallback LLM HTTP {r.status_code}'}
79
-
80
-
81
  def credentials(token):
82
  if not token:
83
  raise RuntimeError('Agent session is missing')
@@ -395,6 +405,7 @@ class ChatInput(BaseModel):
395
 
396
  @router.post('/turn')
397
  def turn(inp:ChatInput,request:Request):
 
398
  with SESSIONS_LOCK:
399
  for sid,s in list(SESSIONS.items()):
400
  if time.monotonic()-s.touched>1800 and not s.busy():del SESSIONS[sid]
@@ -445,9 +456,10 @@ async def local_completion(request:Request):
445
  session=SESSIONS.get(sid)
446
  if not session:raise HTTPException(401,'Unknown local agent session')
447
  data=await request.json()
448
- result=await asyncio.to_thread(dispatch,'llm',dict(data,stream=False))
449
- if result is None:result=await asyncio.to_thread(fallback_completion,data)
450
- if 'error' in result:return JSONResponse(result,status_code=502)
 
451
  if not data.get('stream'):return result
452
  choice=result['choices'][0];message=choice['message'];delta=dict(message)
453
  if message.get('tool_calls'):delta['tool_calls']=[dict(t,index=i) for i,t in enumerate(message['tool_calls'])]
@@ -464,7 +476,9 @@ def runner_auth(request:Request):
464
  @runner_router.get('/next')
465
  def runner_next(request:Request,model:str='',name:str='',provider:str=''):
466
  """Long poll (25 s) for the home-server runner; polling is also its heartbeat."""
467
- runner_auth(request);RUNNER['seen']=time.monotonic()
 
 
468
  if model:RUNNER['llm']={'model':model,'name':name or model,'provider':provider}
469
  deadline=time.monotonic()+25
470
  while (left:=deadline-time.monotonic())>0:
 
33
  from semantic_model import SemanticModel
34
 
35
  DATA = json.loads(Path(__file__).with_name('public_datasets.json').read_text())
36
+ # LLM calls use the configured GPU Ollama model only. TabFM retains its CPU fallback.
37
  RUNNER_TOKEN = os.environ.get('ONTIC_RUNNER_TOKEN', '')
38
+ MODEL = os.environ.get('ONTIC_LLM_MODEL', 'qwen3.8:27b')
 
39
  CHECKPOINT = 'tabicl-classifier-v2-20260212.ckpt'
40
  TABFM_URL = os.environ.get('ONTIC_TABFM_URL', '').rstrip('/')
41
  # What the screen says about the agent comes from here, not from page text (a local run overrides it).
42
+ LLM = {'model':MODEL,'name':os.environ.get('ONTIC_LLM_NAME','Qwen3.8 27B'),'provider':'Ollama'}
43
  STEPS = {'describe_data':1,'show_view':1,'query_ontology':1,'propose_features':2,'execute_features':4,'predict':5,'explain':6,'publish_results':7}
44
  MODEL_LOCK = threading.Lock()
45
  RESULT_LOCK = threading.Lock()
 
53
  return bool(RUNNER_TOKEN) and time.monotonic() - RUNNER['seen'] < 40
54
 
55
 
56
+ def qwen_ready():
57
+ return runner_alive() and (RUNNER.get('llm') or {}).get('model') == MODEL
58
+
59
+
60
  def current_llm():
61
+ return dict(LLM, available=qwen_ready(), route='gpu-runner')
62
 
63
 
64
+ class GPUQwenUnavailable(RuntimeError):
65
+ def __init__(self, reason):
66
+ self.reason = reason
67
+ super().__init__('GPU μ„œλ²„μ˜ Qwen 연결을 ν™•μΈν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. 연결이 볡ꡬ된 λ’€ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.')
68
+
69
+
70
+ def dispatch(kind, payload, claim=15, timeout=180, strict=False):
71
+ """Hand a job to the runner. Strict LLM jobs fail explicitly; TabFM may fall back to CPU."""
72
+ def unavailable(reason):
73
+ if strict:raise GPUQwenUnavailable(reason)
74
  return None
75
+ if not runner_alive():
76
+ return unavailable('runner_offline')
77
+ if strict and not qwen_ready():return unavailable('model_mismatch')
78
  jid = secrets.token_hex(16); job = {'claimed': threading.Event(), 'ready': threading.Event(), 'result': None}
79
  PENDING[jid] = job; JOBS.put({'id': jid, 'kind': kind, 'payload': payload})
80
  try:
81
+ if not job['claimed'].wait(claim):return unavailable('job_not_claimed')
82
+ if not job['ready'].wait(timeout):return unavailable('model_timeout')
83
  result = job['result']
84
+ if not isinstance(result, dict) or 'error' in result:return unavailable('model_failed')
85
+ if strict and result.get('model',MODEL) != MODEL:return unavailable('model_mismatch')
86
+ return result
87
  finally:
88
  PENDING.pop(jid, None)
89
 
90
 
 
 
 
 
 
91
  def credentials(token):
92
  if not token:
93
  raise RuntimeError('Agent session is missing')
 
405
 
406
  @router.post('/turn')
407
  def turn(inp:ChatInput,request:Request):
408
+ if not qwen_ready():raise HTTPException(503,'GPU μ„œλ²„μ˜ Qwen 연결을 기닀리고 μžˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.')
409
  with SESSIONS_LOCK:
410
  for sid,s in list(SESSIONS.items()):
411
  if time.monotonic()-s.touched>1800 and not s.busy():del SESSIONS[sid]
 
456
  session=SESSIONS.get(sid)
457
  if not session:raise HTTPException(401,'Unknown local agent session')
458
  data=await request.json()
459
+ if data.get('model') != MODEL:return JSONResponse({'error':{'message':'Configured Qwen model required','code':'MODEL_MISMATCH'}},status_code=400)
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'])]
 
476
  @runner_router.get('/next')
477
  def runner_next(request:Request,model:str='',name:str='',provider:str=''):
478
  """Long poll (25 s) for the home-server runner; polling is also its heartbeat."""
479
+ runner_auth(request)
480
+ if model != MODEL:raise HTTPException(409,'Configured Qwen model required')
481
+ RUNNER['seen']=time.monotonic()
482
  if model:RUNNER['llm']={'model':model,'name':name or model,'provider':provider}
483
  deadline=time.monotonic()+25
484
  while (left:=deadline-time.monotonic())>0:
web/agent-connection.js CHANGED
@@ -9,9 +9,10 @@ export async function waitForAgent({signal,status,fetchFn=fetch,wait=delay,attem
9
  for(let i=0;i<attempts;i++){
10
  if(signal?.aborted)throw new DOMException('Cancelled','AbortError');
11
  const response=await fetchFn('/api/agent-ready',{signal});
12
- if(response.ok&&(await response.json()).ready===true)return;
13
- status?.('λͺ¨λΈ μ„œλ²„λ₯Ό 깨우고 μžˆμ–΄μš”. μž…λ ₯ν•œ μ§ˆλ¬Έμ€ μ€€λΉ„κ°€ λλ‚˜λ©΄ μ΄μ–΄μ„œ μ „μ†‘ν•©λ‹ˆλ‹€.');
 
14
  if(i+1<attempts)await wait(10000,signal);
15
  }
16
- throw new Error('λͺ¨λΈ μ„œλ²„κ°€ 아직 μ€€λΉ„ μ€‘μž…λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.');
17
  }
 
9
  for(let i=0;i<attempts;i++){
10
  if(signal?.aborted)throw new DOMException('Cancelled','AbortError');
11
  const response=await fetchFn('/api/agent-ready',{signal});
12
+ let state={};try{state=await response.json();}catch{}
13
+ if(response.ok&&state.ready===true)return;
14
+ status?.(state.state==='gpu_offline'?'GPU μ„œλ²„μ˜ Qwen 연결을 기닀리고 μžˆμ–΄μš”. μž…λ ₯ν•œ μ§ˆλ¬Έμ€ μ—°κ²°λ˜λ©΄ μ΄μ–΄μ„œ μ „μ†‘ν•©λ‹ˆλ‹€.':'λͺ¨λΈ μ„œλ²„λ₯Ό 깨우고 μžˆμ–΄μš”. μž…λ ₯ν•œ μ§ˆλ¬Έμ€ μ€€λΉ„κ°€ λλ‚˜λ©΄ μ΄μ–΄μ„œ μ „μ†‘ν•©λ‹ˆλ‹€.');
15
  if(i+1<attempts)await wait(10000,signal);
16
  }
17
+ throw new Error('Qwen λͺ¨λΈ μ„œλ²„μ— μ—°κ²°ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.');
18
  }