DSCI Report: dsci@2191
08:07:27 :: [repository] - index updated from http://sparrowhub.io/repo/api/v1/index run stage: main [task run: task.bash - create directory scm] [task stdout] 08:07:27 :: directory path: scm 08:07:27 :: directory owner: <ubuntu> 08:07:27 :: directory group: <ubuntu> 08:07:27 :: directory access rights: drwxr-xr-x scm: http://localhost:8080/test1.git sha: 007aefe message: keep question by dsci@sparrowhub.io - manual run container runtime: podman raku version Raku++ (rakupp) 3.25.0 — a Raku interpreter and compiler in C++ Implements Raku 6.d, and 6.e under `use v6.e.PREVIEW`. Build v3.25.0 (2026-09-03), x86_64-linux, clang 18.1.3 Home https://raku.online — docs, a tour of the language, and a browser playground sparrow version 0.0.94 runner version: 0.1.1 ======= [task run: task.bash - bash: git config --global advice.detachedHead false] [task stdout] [task run: task.bash - bash: git clone http://localhost:8080/test1.git .] [task stdout] 08:07:28 :: /home/ubuntu/.dsci/.sparky/.cache/007aefe.1789718838/.sparrowdo/scm [task stderr] 08:07:28 :: Cloning into '.'... [task run: task.bash - bash: git checkout 007aefe] [task stdout] 08:07:28 :: /home/ubuntu/.dsci/.sparky/.cache/007aefe.1789718838/.sparrowdo/scm [task stderr] 08:07:28 :: HEAD is now at 007aefe keep question pipeline mode: localhost run dsci agent container from image: melezhik/dsci-agent-rakupp ai_agent_message set: 637265617465204453434920706970656c696e6520746f206465706c6f79204149206167656e74206f6e206c6f63616c686f7374202c2075736520736f6d6520706f70756c6172204167656e74206672616d65776f726b20616e64204c4c4d >>> POST http://127.0.0.1:8181/stash >>> POST http://127.0.0.1:8181/queue >>> wait for jobs, timeout: 600 sec >>> GET http://127.0.0.1:8181/stash/job.run/muardzwelxqhpfbkiojn.4085887 job test ... [OK] ... >>> GET job report, status=200 08:07:37 :: [repository] - index updated from http://sparrowhub.io/repo/api/v1/index run stage: job-run raku version Raku++ (rakupp) 3.25.0 — a Raku interpreter and compiler in C++ Implements Raku 6.d, and 6.e under `use v6.e.PREVIEW`. Build v3.25.0 (2026-09-03), x86_64-linux, clang 18.1.3 Home https://raku.online — docs, a tour of the language, and a browser playground sparrow version 0.0.94 runner version: 0.1.1 run job: test === >>> GET http://127.0.0.1:8181/stash/job.run/muardzwelxqhpfbkiojn.4085887 [task run: task.bash - create directory scm] [task stdout] 08:07:37 :: directory path: scm 08:07:37 :: directory owner: <ubuntu> 08:07:37 :: directory group: <ubuntu> 08:07:37 :: directory access rights: drwxr-xr-x [task run: task.bash - bash: git clone http://localhost:8080/test1.git .] [task stdout] 08:07:37 :: /home/ubuntu/.dsci/.sparky/.cache/muardzwelxqhpfbkiojn.4085887/.sparrowdo/scm [task stderr] 08:07:38 :: Cloning into '.'... [task run: task.bash - bash: git checkout 007aefe] [task stdout] 08:07:38 :: /home/ubuntu/.dsci/.sparky/.cache/muardzwelxqhpfbkiojn.4085887/.sparrowdo/scm [task stderr] 08:07:38 :: HEAD is now at 007aefe keep question [task run: task.bash - bash: prepare dsci scripts] [task stdout] >>> GET http://127.0.0.1:8181/stash/job.run/muardzwelxqhpfbkiojn.4085887 [task run: task.bash - .] [task stdout] 08:07:38 :: total 8 08:07:38 :: -rw-r--r-- 1 ubuntu ubuntu 109 Sep 18 08:07 jobs.yaml 08:07:38 :: -rw-r--r-- 1 ubuntu ubuntu 301 Sep 18 08:07 task.bash 08:07:38 :: 637265617465204453434920706970656c696e6520746f206465706c6f79204149206167656e74206f6e206c6f63616c686f7374202c2075736520736f6d6520706f70756c6172204167656e74206672616d65776f726b20616e64204c4c4d 08:07:46 :: === 08:07:46 :: create DSCI pipeline to deploy AI agent on localhost , use some popular Agent framework and LLM 08:07:46 :: === 08:07:46 :: **File structure** 08:07:46 :: 08:07:46 :: ``` 08:07:46 :: .dsci/ 08:07:46 :: │ 08:07:46 :: ├─ jobs.yaml 08:07:46 :: │ 08:07:46 :: ├─ job_setup/ 08:07:46 :: │ ├─ config.yaml 08:07:46 :: │ ├─ job.py 08:07:46 :: │ └─ tasks/ 08:07:46 :: │ ├─ install_deps/ 08:07:46 :: │ │ └─ task.py 08:07:46 :: │ └─ download_model/ 08:07:46 :: │ └─ task.py 08:07:46 :: │ 08:07:46 :: ├─ job_deploy/ 08:07:46 :: │ ├─ config.yaml 08:07:46 :: │ ├─ job.py 08:07:46 :: │ └─ tasks/ 08:07:46 :: │ └─ start_agent/ 08:07:46 :: │ └─ task.py 08:07:46 :: │ 08:07:46 :: └─ job_test/ 08:07:46 :: ├─ job.bash 08:07:46 :: └─ tasks/ 08:07:46 :: └─ test_endpoint/ 08:07:46 :: └─ task.bash 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: --- 08:07:46 :: 08:07:46 :: ## 1️⃣ `jobs.yaml` – only the pipeline definition 08:07:46 :: 08:07:46 :: ```yaml 08:07:46 :: # .dsci/jobs.yaml 08:07:46 :: jobs: 08:07:46 :: - id: setup 08:07:46 :: path: job_setup/ 08:07:46 :: - id: deploy 08:07:46 :: path: job_deploy/ 08:07:46 :: - id: test 08:07:46 :: path: job_test/ 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: *No matrix, no extra options – just three sequential jobs.* 08:07:46 :: 08:07:46 :: --- 08:07:46 :: 08:07:46 :: ## 2️⃣ Job **setup** – install dependencies and (optionally) download a tiny local model 08:07:46 :: 08:07:46 :: ### `job_setup/config.yaml` – default parameters for the job 08:07:46 :: 08:07:46 :: ```yaml 08:07:46 :: # .dsci/job_setup/config.yaml 08:07:46 :: # Python packages required for the agent 08:07:46 :: requirements: 08:07:46 :: - "langchain==0.1.9" 08:07:46 :: - "uvicorn[standard]==0.27.0" 08:07:46 :: - "fastapi==0.111.0" 08:07:46 :: - "openai==1.30.1" # if you want to hit OpenAI API 08:07:46 :: - "requests==2.32.3" 08:07:46 :: 08:07:46 :: # URL of a very small GGML model (e.g. tiny‑llama) – optional 08:07:46 :: model_url: "https://huggingface.co/ggml-org/ggml-tiny-llama/resolve/main/ggml-tiny-llama.bin" 08:07:46 :: 08:07:46 :: # Where the model will be stored (inside the DSCI artifacts folder) 08:07:46 :: model_path: "${HOME}/artifacts/tiny-llama.bin" 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: ### `job_setup/job.py` – orchestrates the two tasks 08:07:46 :: 08:07:46 :: ```python 08:07:46 :: #!/usr/bin/env python3 08:07:46 :: # .dsci/job_setup/job.py 08:07:46 :: # ------------------------------------------------- 08:07:46 :: # 1️⃣ Install python requirements 08:07:46 :: # 2️⃣ (Optional) download a tiny GGML model 08:07:46 :: # ------------------------------------------------- 08:07:46 :: run_task('install_deps') 08:07:46 :: run_task('download_model') 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: ### `job_setup/tasks/install_deps/task.py` – pip‑install everything 08:07:46 :: 08:07:46 :: ```python 08:07:46 :: #!/usr/bin/env python3 08:07:46 :: # .dsci/job_setup/tasks/install_deps/task.py 08:07:46 :: import subprocess 08:07:46 :: import sys 08:07:46 :: 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # Read the list of packages from the job configuration 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: cfg = config() 08:07:46 :: requirements = cfg.get('requirements', []) 08:07:46 :: 08:07:46 :: if not requirements: 08:07:46 :: print("⚠️ No requirements defined – skipping pip install") 08:07:46 :: else: 08:07:46 :: # Build a single pip command (more reliable than many calls) 08:07:46 :: pip_cmd = [sys.executable, "-m", "pip", "install", "--quiet"] + requirements 08:07:46 :: print(f"🔧 Installing python packages: {', '.join(requirements)}") 08:07:46 :: subprocess.check_call(pip_cmd) 08:07:46 :: 08:07:46 :: # Remember that deps were installed – useful for downstream jobs 08:07:46 :: update_state({"deps_installed": True}) 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: ### `job_setup/tasks/download_model/task.py` – fetch a tiny GGML model (optional) 08:07:46 :: 08:07:46 :: ```python 08:07:46 :: #!/usr/bin/env python3 08:07:46 :: # .dsci/job_setup/tasks/download_model/task.py 08:07:46 :: import os 08:07:46 :: import pathlib 08:07:46 :: import urllib.request 08:07:46 :: 08:07:46 :: cfg = config() 08:07:46 :: model_url = cfg.get("model_url") 08:07:46 :: model_path = cfg.get("model_path") # e.g. ${HOME}/artifacts/tiny-llama.bin 08:07:46 :: 08:07:46 :: if not model_url: 08:07:46 :: print("⚠️ No model URL defined – skipping model download") 08:07:46 :: update_state({"model_downloaded": False}) 08:07:46 :: else: 08:07:46 :: # Ensure the artifacts directory exists 08:07:46 :: artifacts_dir = pathlib.Path.home() / "artifacts" 08:07:46 :: artifacts_dir.mkdir(parents=True, exist_ok=True) 08:07:46 :: 08:07:46 :: # Resolve $HOME in the path 08:07:46 :: model_path = os.path.expandvars(model_path) 08:07:46 :: print(f"⬇️ Downloading model from {model_url}") 08:07:46 :: urllib.request.urlretrieve(model_url, model_path) 08:07:46 :: print(f"✅ Model saved to {model_path}") 08:07:46 :: 08:07:46 :: # Export the absolute path so later jobs can use it 08:07:46 :: update_state({ 08:07:46 :: "model_downloaded": True, 08:07:46 :: "model_path": model_path 08:07:46 :: }) 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: --- 08:07:46 :: 08:07:46 :: ## 3️⃣ Job **deploy** – spin‑up a FastAPI server that runs a LangChain agent 08:07:46 :: 08:07:46 :: ### `job_deploy/config.yaml` – runtime parameters for the agent 08:07:46 :: 08:07:46 :: ```yaml 08:07:46 :: # .dsci/job_deploy/config.yaml 08:07:46 :: # Port on which the agent will be exposed 08:07:46 :: port: 8000 08:07:46 :: 08:07:46 :: # If you want to use OpenAI instead of a local GGML model, set these: 08:07:46 :: openai_api_key: "" # leave empty for local model 08:07:46 :: openai_model: "gpt-4o-mini" 08:07:46 :: 08:07:46 :: # When using the tiny‑llama GGML model (downloaded by the previous job) 08:07:46 :: # the task will read the path from the shared state. 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: ### `job_deploy/job.py` – single task that starts the server 08:07:46 :: 08:07:46 :: ```python 08:07:46 :: #!/usr/bin/env python3 08:07:46 :: # .dsci/job_deploy/job.py 08:07:46 :: # ------------------------------------------------- 08:07:46 :: # Start a FastAPI + LangChain agent. 08:07:46 :: # The server runs in the background (detached) so the job can finish 08:07:46 :: # while the process keeps listening on localhost. 08:07:46 :: # ------------------------------------------------- 08:07:46 :: run_task('start_agent') 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: ### `job_deploy/tasks/start_agent/task.py` – the actual agent code 08:07:46 :: 08:07:46 :: ```python 08:07:46 :: #!/usr/bin/env python3 08:07:46 :: # .dsci/job_deploy/tasks/start_agent/task.py 08:07:46 :: import os 08:07:46 :: import pathlib 08:07:46 :: import subprocess 08:07:46 :: import sys 08:07:46 :: import time 08:07:46 :: import json 08:07:46 :: 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # 1️⃣ Gather configuration 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: cfg = config() 08:07:46 :: port = cfg.get("port", 8000) 08:07:46 :: openai_key = cfg.get("openai_api_key", "").strip() 08:07:46 :: openai_model = cfg.get("openai_model", "gpt-4o-mini") 08:07:46 :: 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # 2️⃣ Retrieve the model path from the previous job (if any) 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: state = config() # whole pipeline state 08:07:46 :: model_path = state.get("_dsci_", {}) \ 08:07:46 :: .get("setup", {}) \ 08:07:46 :: .get("model_path") # may be None 08:07:46 :: 08:07:46 :: use_local_model = bool(model_path and pathlib.Path(model_path).exists()) 08:07:46 :: 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # 3️⃣ Build a tiny FastAPI app on‑the‑fly 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: app_code = pathlib.Path("/tmp/agent_app.py") 08:07:46 :: app_code.write_text(f""" 08:07:46 :: import os 08:07:46 :: from fastapi import FastAPI, HTTPException 08:07:46 :: from pydantic import BaseModel 08:07:46 :: from typing import Any 08:07:46 :: 08:07:46 :: app = FastAPI() 08:07:46 :: 08:07:46 :: class Prompt(BaseModel): 08:07:46 :: text: str 08:07:46 :: 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # LangChain / OpenAI or local GGML setup 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: if {json.dumps(use_local_model)}: 08:07:46 :: # ---------- LOCAL GGML MODEL ---------- 08:07:46 :: # Very simple placeholder – replace with your own inference wrapper 08:07:46 :: # For demo purposes we just echo the prompt. 08:07:46 :: def _run(prompt: str) -> str: 08:07:46 :: return f"[LOCAL MODEL] {{prompt}}" 08:07:46 :: else: 08:07:46 :: # ---------- OPENAI ---------- 08:07:46 :: import openai 08:07:46 :: openai.api_key = {json.dumps(openai_key)} 08:07:46 :: def _run(prompt: str) -> str: 08:07:46 :: resp = openai.ChatCompletion.create( 08:07:46 :: model={json.dumps(openai_model)}, 08:07:46 :: messages=[{{"role": "user", "content": prompt}}] 08:07:46 :: ) 08:07:46 :: return resp.choices[0].message.content 08:07:46 :: 08:07:46 :: @app.post("/run") 08:07:46 :: def run_prompt(p: Prompt) -> Any: 08:07:46 :: try: 08:07:46 :: answer = _run(p.text) 08:07:46 :: return {{"answer": answer}} 08:07:46 :: except Exception as e: 08:07:46 :: raise HTTPException(status_code=500, detail=str(e)) 08:07:46 :: """) 08:07:46 :: 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # 4️⃣ Launch the server in background (detached) 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # We keep a tiny “ready” file to signal that the service is up 08:07:46 :: ready_file = pathlib.Path(os.path.expanduser("~/artifacts/agent_ready")) 08:07:46 :: if ready_file.exists(): 08:07:46 :: ready_file.unlink() 08:07:46 :: 08:07:46 :: # Use `nohup` + `&` so the job can exit while the server lives 08:07:46 :: cmd = [ 08:07:46 :: sys.executable, "-m", "uvicorn", 08:07:46 :: "agent_app:app", 08:07:46 :: "--host", "127.0.0.1", 08:07:46 :: "--port", str(port), 08:07:46 :: "--log-level", "error" 08:07:46 :: ] 08:07:46 :: print(f"🚀 Starting FastAPI agent on http://127.0.0.1:{port}") 08:07:46 :: proc = subprocess.Popen( 08:07:46 :: cmd, 08:07:46 :: cwd="/tmp", 08:07:46 :: stdout=subprocess.DEVNULL, 08:07:46 :: stderr=subprocess.DEVNULL, 08:07:46 :: preexec_fn=os.setsid # detach from parent 08:07:46 :: ) 08:07:46 :: 08:07:46 :: # Wait a couple of seconds for the server to bind 08:07:46 :: for _ in range(10): 08:07:46 :: try: 08:07:46 :: import socket 08:07:46 :: with socket.create_connection(("127.0.0.1", port), timeout=1): 08:07:46 :: break 08:07:46 :: except OSError: 08:07:46 :: time.sleep(0.5) 08:07:46 :: else: 08:07:46 :: print("❌ Agent failed to start") 08:07:46 :: sys.exit(1) 08:07:46 :: 08:07:46 :: # Touch the ready‑file so downstream jobs can see the service 08:07:46 :: ready_file.touch() 08:07:46 :: print("✅ Agent is ready") 08:07:46 :: 08:07:46 :: # Export useful state for later jobs (pid & port) 08:07:46 :: update_state({ 08:07:46 :: "agent_pid": proc.pid, 08:07:46 :: "agent_port": port, 08:07:46 :: "agent_ready_file": str(ready_file) 08:07:46 :: }) 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: *The task creates a temporary `agent_app.py` (FastAPI) and launches it with `uvicorn`. 08:07:46 :: It stores the PID, port and a “ready” flag in the DSCI state – downstream jobs can read those values.* 08:07:46 :: 08:07:46 :: --- 08:07:46 :: 08:07:46 :: ## 4️⃣ Job **test** – sanity‑check the running agent 08:07:46 :: 08:07:46 :: ### `job_test/job.bash` – simply run the single test task 08:07:46 :: 08:07:46 :: ```bash 08:07:46 :: #!/usr/bin/env bash 08:07:46 :: # .dsci/job_test/job.bash 08:07:46 :: run_task "test_endpoint" 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: ### `job_test/tasks/test_endpoint/task.bash` – call the `/run` endpoint 08:07:46 :: 08:07:46 :: ```bash 08:07:46 :: #!/usr/bin/env bash 08:07:46 :: # .dsci/job_test/tasks/test_endpoint/task.bash 08:07:46 :: 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # Grab configuration / state 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: PORT=$(config port) # from job_deploy/config.yaml (inherited) 08:07:46 :: READY_FILE=$(config agent_ready_file) # exported by the deploy job 08:07:46 :: 08:07:46 :: # Wait until the ready file appears (max 15 seconds) 08:07:46 :: SECONDS_WAITED=0 08:07:46 :: while [[ ! -f "$READY_FILE" && $SECONDS_WAITED -lt 15 ]]; do 08:07:46 :: echo "⏳ Waiting for agent to become ready..." 08:07:46 :: sleep 1 08:07:46 :: ((SECONDS_WAITED++)) 08:07:46 :: done 08:07:46 :: 08:07:46 :: if [[ ! -f "$READY_FILE" ]]; then 08:07:46 :: echo "❌ Agent did not signal readiness – aborting test" 08:07:46 :: exit 1 08:07:46 :: fi 08:07:46 :: 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: # Perform a simple request 08:07:46 :: # ----------------------------------------------------------------- 08:07:46 :: PROMPT="What is the current year?" 08:07:46 :: RESPONSE=$(curl -s -X POST "http://127.0.0.1:${PORT}/run" \ 08:07:46 :: -H "Content-Type: application/json" \ 08:07:46 :: -d "$(printf '{"text":"%s"}' "$PROMPT")") 08:07:46 :: 08:07:46 :: echo "🗣️ Prompt sent: $PROMPT" 08:07:46 :: echo "📨 Raw response: $RESPONSE" 08:07:46 :: 08:07:46 :: # Extract the answer field (jq is available in the default container) 08:07:46 :: ANSWER=$(echo "$RESPONSE" | jq -r '.answer // "❓ no answer"') 08:07:46 :: echo "✅ Agent answered: $ANSWER" 08:07:46 :: ``` 08:07:46 :: 08:07:46 :: *The Bash task reads the port from the shared configuration [task stderr] 08:07:46 :: 08:07:46 :: real 0m8.038s 08:07:46 :: user 0m0.708s 08:07:46 :: sys 0m0.121s saving job artifacts ... >> PUT http://127.0.0.1:8181/file/project/dsci/job/007aefe.1789718838/filename/answer.md >> PUT http://127.0.0.1:8181/file/project/dsci/job/007aefe.1789718838/filename/question.txt >>> POST http://127.0.0.1:8181/stash --- ---