Skip to content

Commit 08a6d2b

Browse files
Merge pull request #146 from Annotation-Garden/feature/issue-144-persistent-lsp
feat: persistent hed-lsp client (closes #144)
2 parents cf0baf4 + 3250f94 commit 08a6d2b

26 files changed

Lines changed: 1743 additions & 1217 deletions

.env.example

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -101,18 +101,27 @@ HED_SCHEMA_VERSION=8.4.0
101101
# ============================================================================
102102
# HED-LSP Configuration (Recommended)
103103
# ============================================================================
104-
# HEDit can use hed-lsp CLI for HED tag suggestions.
105-
# Install: git clone https://github.com/hed-standard/hed-lsp.git
106-
# cd hed-lsp/server && npm install && npm run compile && npm link
104+
# HEDit holds one persistent connection to the hed-lsp Node server for the
105+
# lifetime of the process (server mode) or the command (standalone CLI).
106+
# Install hed-lsp: git clone https://github.com/hed-standard/hed-lsp.git
107+
# cd hed-lsp/server && npm install && npm run compile
107108
#
108-
# Once installed, the 'hed-suggest' command will be available in PATH.
109-
# HEDit auto-detects hed-lsp availability - no configuration needed!
110-
111-
# Enable semantic search for better tag suggestions (requires embeddings)
112-
# HED_LSP_USE_SEMANTIC=false
113-
114-
# Maximum number of tag suggestions to return
115-
# HED_LSP_MAX_RESULTS=10
109+
# Path to the built server.js. Inside the official Docker image this is
110+
# baked in at /app/hed-lsp/server/out/server.js. Outside Docker, point it
111+
# at your local checkout.
112+
# HED_LSP_SERVER_JS=/path/to/hed-lsp/server/out/server.js
113+
114+
# Set to 1 to disable the persistent LSP connection entirely (the workflow
115+
# will run with keyword-only preprocessing, no LSP tag enrichment).
116+
# HED_LSP_DISABLE=0
117+
118+
# Override the per-user runtime directory used by `hedit lsp start` to
119+
# place its socket and PID file.
120+
# HEDIT_LSP_RUNTIME_DIR=/path/to/runtime/dir
121+
122+
# Set HEDIT_LSP=0 to make `hedit annotate` ignore an already-running
123+
# daemon (useful when debugging without the LSP path).
124+
# HEDIT_LSP=1
116125

117126
# ============================================================================
118127
# Legacy JavaScript Validator (Deprecated)

.github/workflows/test.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,24 @@ jobs:
4747
with:
4848
python-version: ${{ matrix.python-version }}
4949

50+
- name: Set up Node.js for hed-lsp
51+
uses: actions/setup-node@v6
52+
with:
53+
node-version: '22'
54+
55+
- name: Build hed-lsp (for tests/lsp/ real-LSP tests)
56+
env:
57+
HED_LSP_REF: 36447afb5e48bfecc45af0464b652614ba571414
58+
run: |
59+
git clone https://github.com/hed-standard/hed-lsp.git "$RUNNER_TEMP/hed-lsp"
60+
git -C "$RUNNER_TEMP/hed-lsp" checkout --quiet "${HED_LSP_REF}"
61+
cd "$RUNNER_TEMP/hed-lsp"
62+
corepack enable
63+
corepack prepare pnpm@10.33.4 --activate
64+
pnpm install --frozen-lockfile
65+
pnpm run -r compile
66+
echo "HED_LSP_SERVER_JS=$RUNNER_TEMP/hed-lsp/server/out/server.js" >> "$GITHUB_ENV"
67+
5068
- name: Install uv
5169
uses: astral-sh/setup-uv@v7
5270

Dockerfile

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,27 @@ WORKDIR /app
4141

4242
# Clone HED repositories (self-contained)
4343
# Using schemas_latest_json from official hed-schemas repo (JSON inheritance fix now merged)
44+
# hed-lsp is pinned to the branch that adds the `hed/suggest` JSON-RPC
45+
# request handler; once that lands upstream this can switch back to a
46+
# tag-based clone of main.
47+
ARG HED_LSP_REF=feat/hed-suggest-request
4448
RUN git clone --depth 1 https://github.com/hed-standard/hed-schemas.git /app/hed-schemas && \
4549
git clone --depth 1 https://github.com/hed-standard/hed-javascript.git /app/hed-javascript && \
46-
git clone --depth 1 https://github.com/hed-standard/hed-lsp.git /app/hed-lsp
50+
git clone --depth 1 --branch "${HED_LSP_REF}" https://github.com/hed-standard/hed-lsp.git /app/hed-lsp
4751

4852
# Build HED JavaScript validator
4953
WORKDIR /app/hed-javascript
5054
RUN npm install && npm run build
5155

52-
# Build and install hed-suggest CLI from hed-lsp server
56+
# Build the hed-lsp server. As of the npm-to-pnpm migration in hed-lsp,
57+
# the repo uses pnpm workspaces; install pnpm via corepack so the
58+
# workspace's lockfile and server/node_modules resolve correctly.
59+
# The compiled server.js at /app/hed-lsp/server/out/server.js is what
60+
# the FastAPI lifespan spawns via HED_LSP_SERVER_JS.
5361
WORKDIR /app/hed-lsp
54-
RUN npm install && npm run compile
55-
WORKDIR /app/hed-lsp/server
56-
RUN npm install && npm link
62+
RUN corepack enable && corepack prepare pnpm@10.33.4 --activate && \
63+
pnpm install --frozen-lockfile && \
64+
pnpm run -r compile
5765

5866
# Return to app directory
5967
WORKDIR /app
@@ -74,6 +82,7 @@ RUN pip install uv && \
7482
# Set environment variables for HED resources (internal paths)
7583
ENV HED_SCHEMA_DIR=/app/hed-schemas/schemas_latest_json \
7684
HED_VALIDATOR_PATH=/app/hed-javascript \
85+
HED_LSP_SERVER_JS=/app/hed-lsp/server/out/server.js \
7786
USE_JS_VALIDATOR=true
7887

7988
# Expose port

deploy/Dockerfile

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,28 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
2323
rm -rf /var/lib/apt/lists/*
2424

2525
# Clone HED repositories (self-contained)
26-
# Using schemas_latest_json from official hed-schemas repo (JSON inheritance fix now merged)
26+
# hed-lsp is pinned to the merge commit on main that landed the
27+
# `hed/suggest` JSON-RPC request (hed-standard/hed-lsp#31, v0.4.0).
28+
# Pinning to a SHA (not a tag/branch) so the image is reproducible.
29+
ARG HED_LSP_REF=36447afb5e48bfecc45af0464b652614ba571414
2730
RUN git clone --depth 1 https://github.com/hed-standard/hed-schemas.git /app/hed-schemas && \
2831
git clone --depth 1 https://github.com/hed-standard/hed-javascript.git /app/hed-javascript && \
29-
git clone --depth 1 https://github.com/hed-standard/hed-lsp.git /app/hed-lsp
32+
git clone https://github.com/hed-standard/hed-lsp.git /app/hed-lsp && \
33+
git -C /app/hed-lsp checkout --quiet "${HED_LSP_REF}"
3034

3135
# Build HED JavaScript validator
3236
WORKDIR /app/hed-javascript
3337
RUN npm install && npm run build
3438

35-
# Build and install hed-suggest CLI from hed-lsp server
39+
# Build the hed-lsp server. As of the npm-to-pnpm migration in hed-lsp,
40+
# the repo uses pnpm workspaces; install pnpm via corepack so the
41+
# workspace's lockfile and server/node_modules resolve correctly.
42+
# The compiled server.js at /app/hed-lsp/server/out/server.js is what
43+
# the FastAPI lifespan spawns via HED_LSP_SERVER_JS.
3644
WORKDIR /app/hed-lsp
37-
RUN npm install && npm run compile
38-
WORKDIR /app/hed-lsp/server
39-
RUN npm install && npm link
45+
RUN corepack enable && corepack prepare pnpm@10.33.4 --activate && \
46+
pnpm install --frozen-lockfile && \
47+
pnpm run -r compile
4048

4149
# Return to app directory
4250
WORKDIR /app
@@ -56,6 +64,7 @@ COPY src /app/src
5664
# Set environment variables for HED resources (internal paths)
5765
ENV HED_SCHEMA_DIR=/app/hed-schemas/schemas_latest_json \
5866
HED_VALIDATOR_PATH=/app/hed-javascript \
67+
HED_LSP_SERVER_JS=/app/hed-lsp/server/out/server.js \
5968
USE_JS_VALIDATOR=true
6069

6170
# Expose the application port

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "hedit"
7-
version = "0.7.9a2"
7+
version = "0.7.10a0"
88
description = "Multi-agent system for HED annotation generation and validation"
99
readme = "PKG_README.md"
1010
requires-python = ">=3.12"
@@ -39,6 +39,7 @@ dependencies = [
3939

4040
[project.scripts]
4141
hedit = "src.cli.main:cli"
42+
hedit-lspd = "src.lsp.daemon:main"
4243

4344
[project.urls]
4445
Homepage = "https://annotation.garden/hedit"

src/agents/validation_agent.py

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
from pathlib import Path
1313

1414
from src.agents.state import HedAnnotationState
15+
from src.lsp import HedLspClient
1516
from src.utils.error_remediation import get_remediator
1617
from src.utils.schema_loader import HedSchemaLoader
17-
from src.validation.hed_lsp import is_hed_lsp_available, suggest_tags_for_keywords
1818
from src.validation.hed_validator import (
1919
HedJavaScriptValidator,
2020
HedPythonValidator,
@@ -81,7 +81,7 @@ def __init__(
8181
use_javascript: bool = True,
8282
validator_path: Path | None = None,
8383
tests_json_path: Path | str | None = None,
84-
use_hed_lsp: bool = True,
84+
lsp_client: HedLspClient | None = None,
8585
) -> None:
8686
"""Initialize the validation agent.
8787
@@ -90,13 +90,14 @@ def __init__(
9090
use_javascript: Whether to use JavaScript validator (more detailed)
9191
validator_path: Path to hed-javascript repository (required if use_javascript=True)
9292
tests_json_path: Optional path to javascriptTests.json for error remediation
93-
use_hed_lsp: Whether to use hed-lsp for tag suggestions (auto-detected)
93+
lsp_client: Pre-built HedLspClient for tag-replacement suggestions
94+
when validation finds invalid tags. None disables suggestions.
9495
"""
9596
self.schema_loader = schema_loader
9697
self.use_javascript = use_javascript
9798
self.validator_path = validator_path
9899
self.error_remediator = get_remediator(tests_json_path)
99-
self.use_hed_lsp = use_hed_lsp and is_hed_lsp_available()
100+
self.lsp_client = lsp_client
100101

101102
# Validator is lazily initialized on first use via _get_or_create_validator
102103
self._validator: HedJavaScriptValidator | HedPythonValidator | None = None
@@ -134,34 +135,37 @@ def _extract_problematic_tags(self, errors: list, warnings: list) -> list[str]:
134135

135136
return problematic_tags
136137

137-
def _get_tag_suggestions(
138-
self, problematic_tags: list[str], schema_version: str
139-
) -> dict[str, list[str]]:
140-
"""Get suggested valid tags for problematic tags using hed-lsp.
138+
async def _get_tag_suggestions(self, problematic_tags: list[str]) -> dict[str, list[str]]:
139+
"""Get suggested valid tags for problematic tags via persistent LSP.
141140
142141
Args:
143142
problematic_tags: List of problematic tag names
144-
schema_version: HED schema version
145143
146144
Returns:
147-
Dictionary mapping problematic tags to suggested alternatives
145+
Dictionary mapping each problematic tag to a list of suggested
146+
valid alternatives. Empty if no LSP client is configured or the
147+
server returned no matches.
148148
"""
149-
if not self.use_hed_lsp or not problematic_tags:
149+
if self.lsp_client is None or not problematic_tags:
150150
return {}
151151

152152
try:
153-
return suggest_tags_for_keywords(
154-
problematic_tags,
155-
schema_version=schema_version,
156-
max_results=5, # Limit suggestions for clarity
157-
)
158-
except (RuntimeError, OSError) as e:
153+
result = await self.lsp_client.suggest(*problematic_tags)
154+
except Exception as exc:
155+
# `suggest()` already converts transport errors to a failure
156+
# result; anything that lands here is a programming error and
157+
# deserves a traceback.
159158
logger.warning(
160-
"Failed to get tag suggestions from hed-lsp for tags %s: %s",
159+
"hed-lsp suggest call failed for tags %s: %s",
161160
problematic_tags,
162-
e,
161+
exc,
162+
exc_info=True,
163163
)
164164
return {}
165+
if not result.success:
166+
logger.debug("hed-lsp suggest returned failure: %s", result.error)
167+
return {}
168+
return result.raw
165169

166170
async def validate(self, state: HedAnnotationState) -> dict:
167171
"""Validate the current HED annotation.
@@ -202,10 +206,10 @@ async def validate(self, state: HedAnnotationState) -> dict:
202206

203207
# Extract problematic tags and get suggestions from hed-lsp
204208
tag_suggestions: dict[str, list[str]] = {}
205-
if not result.is_valid and self.use_hed_lsp:
209+
if not result.is_valid and self.lsp_client is not None:
206210
problematic_tags = self._extract_problematic_tags(result.errors, result.warnings)
207211
if problematic_tags:
208-
tag_suggestions = self._get_tag_suggestions(problematic_tags, schema_version)
212+
tag_suggestions = await self._get_tag_suggestions(problematic_tags)
209213

210214
# Determine validation status
211215
validation_attempts = state["validation_attempts"] + 1

0 commit comments

Comments
 (0)