Spaces:
Running
Running
Deploy 444e366 from GitHub Actions
Browse files- README.md +3 -1
- __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
- app.py +2 -1
- public_agent.py +34 -20
- web/agent-connection.js +4 -3
README.md
CHANGED
|
@@ -12,4 +12,6 @@ ONTIC κ³΅κ° μ ν 미리보기μμ μ¬μ©νλ ν©μ± λ°λͺ¨ λ° μ¬μ©μ
|
|
| 12 |
|
| 13 |
`/predict-upload`λ μ΅λͺ
μ«μ νλ ¬κ³Ό μ λ΅, μκ³μ΄ λ μ§λ₯Ό λ°μ μ€μ TabICL μΆλ‘ κ³Ό 보λ₯ κ²μ¦μ μ€ννλ€. μ
λ ₯ νμ μ μ₯νμ§ μλλ€. λΆλ₯Β·νκ·λ μ΅λ 1,000κ° κ³Όκ±° μ¬λ‘Β·30κ° λ³μΒ·100κ° μμΈ‘ λμ, μκ³μ΄μ μ΅λ 1,000κ° κ΄μΈ‘κ³Ό 24κ° λ―Έλ κΈ°κ°μΌλ‘ μ ννλ€.
|
| 14 |
|
| 15 |
-
κ³΅κ° μ±ν
μ μ΅μ΄ λ΄λΆ λ°λͺ¨μ 곡μ νλ `SessionRuntime`Β·μν ν둬ννΈΒ·νΌμ² νλ‘κ·Έλ¨ κ²μ¦κΈ°λ₯Ό μ¬μ©νλ€.
|
|
|
|
|
|
|
|
|
| 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
|
| 37 |
RUNNER_TOKEN = os.environ.get('ONTIC_RUNNER_TOKEN', '')
|
| 38 |
-
|
| 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('
|
| 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
|
| 59 |
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
| 69 |
-
|
| 70 |
result = job['result']
|
| 71 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 449 |
-
|
| 450 |
-
|
|
|
|
| 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)
|
|
|
|
|
|
|
| 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 |
-
|
| 13 |
-
|
|
|
|
| 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 |
}
|