DSCI Report: dsci@2191


artifacts
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
---

---