Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
00f621e
docs: capture TOCHI prototype roadmap
akgohain Apr 26, 2026
d16987a
feat: add closed-loop workflow evidence spine
akgohain Apr 26, 2026
9b9940a
feat: improve project mounting and volume intake
akgohain Apr 26, 2026
f51d31e
feat: make assistant actions persistent and runnable
akgohain Apr 26, 2026
e71a141
feat: summarize model runtime execution
akgohain Apr 26, 2026
d13aecd
feat: streamline proofreading workspace
akgohain Apr 26, 2026
3ba7a9e
fix: clarify assistant and file picker flows
akgohain Apr 26, 2026
4be9a17
style: retint app accent color
akgohain Apr 26, 2026
919998e
fix: harden assistant routing and file help
akgohain Apr 26, 2026
8cb2c3b
feat: make agent training workflow-led
akgohain Apr 26, 2026
a6ff6fb
feat(app): tighten assistant shell workflow
akgohain Apr 27, 2026
de77772
feat(files): confirm project roles before setup
akgohain Apr 27, 2026
ae8feaf
feat(workflows): persist config lineage and bundle exports
akgohain Apr 27, 2026
b9ba41c
docs: record prototype checkpoint evidence
akgohain Apr 27, 2026
514a3af
massive rework
akgohain Apr 29, 2026
ac940e4
fix(agent): recognize training requests
akgohain Apr 29, 2026
449578a
docs: add remote codex handoff
akgohain Apr 29, 2026
5759278
docs: vendor codex working memory
akgohain Apr 29, 2026
eb5cce6
feat(projects): confirm mounted project context
akgohain May 5, 2026
1b80965
feat(workflows): add progress-aware agent commands
akgohain May 5, 2026
c1f3d7a
feat(assistant): keep workflow chat continuous
akgohain May 5, 2026
143d127
feat(viewer): harden visualization and proofreading
akgohain May 5, 2026
4ea4bb1
fix(runtime): launch multi-volume training subsets
akgohain May 5, 2026
3292ff7
chore(logging): capture detailed app events
akgohain May 5, 2026
c151bf9
docs: record agent prototype progress
akgohain May 5, 2026
d706244
feat(workflows): harden project memory and provenance
akgohain Jun 4, 2026
302a373
feat(auth): add project user management foundations
akgohain Jun 4, 2026
e24c205
feat(ui): surface workflow agents and editable action cards
akgohain Jun 4, 2026
e364ffb
test(demo): add Yixiao case study gates
akgohain Jun 4, 2026
a047f34
docs(research): capture paper prototype roadmap
akgohain Jun 4, 2026
67b0e0d
Harden TOCHI demo deployment and case-study workflow
akgohain Jul 14, 2026
8b6a935
Merge TOCHI workflow prototype into generalist app
akgohain Jul 14, 2026
b0d2e0b
fix(bootstrap): restore reproducible clean installs
akgohain Jul 15, 2026
bef067c
fix(workflows): remove deployment-specific runtime assumptions
akgohain Jul 15, 2026
cfcd133
ci: add clean backend and frontend verification
akgohain Jul 15, 2026
97590c9
ci: declare pytest development dependency
akgohain Jul 15, 2026
04289a1
style(python): establish Black 26.5 baseline
akgohain Jul 15, 2026
e814615
ci: allow linter commit statuses
akgohain Jul 15, 2026
5abf922
style: establish Prettier 3.8 baseline
akgohain Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __pycache__
.DS_Store
node_modules
.venv
pytorch_connectomics
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: CI

permissions:
contents: read

"on":
push:
branches:
- main
pull_request:
branches:
- main

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
backend:
name: Backend tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.11"

- name: Set up uv
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
with:
version: "0.7.3"
enable-cache: true

- name: Fetch pinned PyTorch Connectomics runtime
run: bash scripts/setup_pytorch_connectomics.sh

- name: Install Python dependencies
run: uv sync --frozen --python 3.11 --group dev

- name: Run backend tests
run: uv run pytest -q

frontend:
name: Frontend tests and build
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: client
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"
cache: npm
cache-dependency-path: client/package-lock.json

- name: Install frontend dependencies
run: npm ci --fetch-retries=5 --fetch-retry-maxtimeout=120000

- name: Run frontend tests
run: npm test -- --runInBand
env:
CI: "true"

- name: Build frontend
run: npm run build
12 changes: 10 additions & 2 deletions .github/workflows/superlinter.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
---
name: Super-Linter

permissions:
contents: read
statuses: write

"on":
push:
branches:
Expand All @@ -15,14 +19,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false

- name: Run Super-Linter
uses: github/super-linter@v4
uses: super-linter/super-linter@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_ALL_CODEBASE: false
ENABLE_GITHUB_PULL_REQUEST_SUMMARY_COMMENT: false
VALIDATE_CSS_PRETTIER: true
VALIDATE_HTML_PRETTIER: true
VALIDATE_JAVASCRIPT_PRETTIER: true
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ sql_app.db
uploads
pytorch_connectomics
server_api/chatbot/faiss_index/
.logs/
.logs/

# Local deployment evidence (may contain transient viewer URLs)
demo-proofread-3d.png
docs/case-studies/yixiao-postdeploy-smoke-report-*.json
docs/case-studies/yixiao-postdeploy-smoke-report-*.md
docs/case-studies/yixiao-synthetic-walkthrough-report-*.md
6 changes: 2 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | sh -s -- --install-dir /usr/loc
WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --python 3.11

COPY scripts/setup_pytorch_connectomics.sh ./scripts/setup_pytorch_connectomics.sh
RUN chmod +x scripts/setup_pytorch_connectomics.sh && \
./scripts/setup_pytorch_connectomics.sh --force && \
uv pip install --directory /app --editable /app/pytorch_connectomics && \
./scripts/setup_pytorch_connectomics.sh && \
uv sync --frozen --no-dev --python 3.11 && \
rm -rf /app/pytorch_connectomics/.git

COPY server_api ./server_api
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ scripts\bootstrap.ps1 # Windows

Re-run the relevant bootstrap script when you need to update dependencies.

The bootstrap scripts fetch the legacy-compatible PyTorch Connectomics runtime
from `pytc-client-legacy-runtime` and verify commit
`04c2a35e78a1a7ca1138f83a98fc3ef27097abd4` before installation. The generated
`pytorch_connectomics/` checkout is intentionally not tracked by this repository.

## Run the app

```
Expand All @@ -35,6 +40,10 @@ Optional runtime environment variables:
PYTC_AUTH_SECRET=replace-me
PYTC_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,null
PYTC_NEUROGLANCER_PUBLIC_BASE=http://localhost:4244
OLLAMA_MODEL=qwen3.6:27b
PYTC_WORKFLOW_INTENT_MODEL=qwen3.6:27b
OLLAMA_EMBED_MODEL=qwen3-embedding:8b
PYTC_UNLOAD_OLLAMA_BEFORE_TRAINING=0 # set to 1 only on memory-constrained GPUs
```

## Chatbot Docs Index
Expand All @@ -54,6 +63,11 @@ You can override the embeddings endpoint if needed:
OLLAMA_BASE_URL=http://cscigpu08.bc.edu:4443 uv run python server_api/chatbot/update_faiss.py
```

The local assistant and workflow intent classifier default to `qwen3.6:27b` with
`qwen3-embedding:8b`, matching the bundled FAISS index settings. For
experimental local model upgrades, see
`docs/research/on-device-llm-options-2026-04-26.md`.

If restarting after a crash or interrupted session, kill any lingering processes first:

```
Expand Down
200 changes: 200 additions & 0 deletions app_event_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
from __future__ import annotations

import json
import logging
import os
import sys
import threading
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

_ROOT_DIR = Path(__file__).resolve().parent
_DEFAULT_LOG_PATH = _ROOT_DIR / ".logs" / "app" / "app-events.jsonl"
_ORIGINAL_STDOUT = sys.stdout
_ORIGINAL_STDERR = sys.stderr
_WRITE_LOCK = threading.Lock()
_CONFIG_LOCK = threading.Lock()
_CONFIGURED_COMPONENTS: set[str] = set()
_STDIO_REDIRECTED = False


def get_app_event_log_path() -> Path:
raw_path = os.getenv("PYTC_APP_EVENT_LOG_PATH", "").strip()
if raw_path:
return Path(raw_path).expanduser().resolve(strict=False)
return _DEFAULT_LOG_PATH


def _detect_stream_level(default_level: str, message: str) -> str:
text = (message or "").strip()
upper = text.upper()
if upper.startswith("INFO:") or " INFO:" in upper:
return "INFO"
if (
upper.startswith("WARNING:")
or upper.startswith("WARN:")
or " WARNING:" in upper
):
return "WARNING"
if upper.startswith("DEBUG:") or " DEBUG:" in upper:
return "DEBUG"
if upper.startswith("ERROR:") or " ERROR:" in upper:
return "ERROR"
if "TRACEBACK" in upper or "EXCEPTION" in upper or "FAILED" in upper:
return "ERROR"
return default_level.upper()


def _normalize_value(value: Any) -> Any:
if value is None or isinstance(value, (bool, int, float, str)):
return value
if isinstance(value, Path):
return str(value)
if isinstance(value, dict):
return {str(key): _normalize_value(val) for key, val in value.items()}
if isinstance(value, (list, tuple, set)):
return [_normalize_value(item) for item in value]
return str(value)


def append_app_event(
*,
component: str,
event: str,
level: str = "INFO",
message: str | None = None,
**fields: Any,
) -> dict[str, Any]:
log_path = get_app_event_log_path()
log_path.parent.mkdir(parents=True, exist_ok=True)

record = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"pid": os.getpid(),
"component": component,
"event": event,
"level": level.upper(),
"message": message or "",
}
record.update(
{
key: _normalize_value(value)
for key, value in fields.items()
if value is not None
}
)

with _WRITE_LOCK:
with log_path.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=True) + "\n")

return record


class _AppEventStream:
def __init__(
self,
*,
component: str,
stream_name: str,
level: str,
original_stream,
) -> None:
self.component = component
self.stream_name = stream_name
self.level = level
self.original_stream = original_stream
self._buffer = ""

def write(self, data) -> int:
text = str(data)
if not text:
return 0

self.original_stream.write(text)
self._buffer += text

while "\n" in self._buffer:
line, self._buffer = self._buffer.split("\n", 1)
if line.strip():
append_app_event(
component=self.component,
event=f"{self.stream_name}_line",
level=_detect_stream_level(self.level, line),
message=line,
stream=self.stream_name,
)

return len(text)

def flush(self) -> None:
self.original_stream.flush()
if self._buffer.strip():
append_app_event(
component=self.component,
event=f"{self.stream_name}_line",
level=_detect_stream_level(self.level, self._buffer),
message=self._buffer,
stream=self.stream_name,
)
self._buffer = ""

def isatty(self) -> bool:
return bool(getattr(self.original_stream, "isatty", lambda: False)())

def fileno(self) -> int:
return self.original_stream.fileno()


def configure_process_logging(component: str) -> Path:
global _STDIO_REDIRECTED

with _CONFIG_LOCK:
if component in _CONFIGURED_COMPONENTS:
return get_app_event_log_path()

if not _STDIO_REDIRECTED:
sys.stdout = _AppEventStream(
component=component,
stream_name="stdout",
level="INFO",
original_stream=_ORIGINAL_STDOUT,
)
sys.stderr = _AppEventStream(
component=component,
stream_name="stderr",
level="ERROR",
original_stream=_ORIGINAL_STDERR,
)
_STDIO_REDIRECTED = True

root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
if not root_logger.handlers:
logging.basicConfig(
level=logging.INFO,
stream=sys.stdout,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
else:
for handler in root_logger.handlers:
if not isinstance(handler, logging.StreamHandler):
continue
stream = getattr(handler, "stream", None)
if stream is _ORIGINAL_STDOUT:
handler.setStream(sys.stdout)
elif stream is _ORIGINAL_STDERR:
handler.setStream(sys.stderr)

append_app_event(
component=component,
event="process_logging_configured",
level="INFO",
message=f"{component} logging configured",
log_path=str(get_app_event_log_path()),
configured_at_ms=round(time.time() * 1000),
)
_CONFIGURED_COMPONENTS.add(component)
return get_app_event_log_path()
Loading