feat: enforce hard test-tier requirement across all DevPlace workflow agents and feature-builder docs
DevPlace CI / test (push) Failing after 21m53s
DevPlace CI / test (push) Failing after 21m53s
Update the feature-builder agent prompt, test-maintainer agent, and all four workflow JS files (devii-tool, endpoint, feature, job-service) to codify the DevPlace test standard as a non-optional project requirement: one test file per endpoint, directory tree mirroring the URL/source path, split into three tiers (unit, api, e2e). Add explicit Test phases to devii-tool, endpoint, feature, and job-service workflows, and embed tier-specific test instructions (path mapping, fixture choice, coverage scope) directly in each workflow's meta description and TESTS constant.
This commit is contained in:
@@ -6,7 +6,8 @@ import hashlib
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from devplacepy import stealth
|
||||
from devplacepy.config import INTERNAL_EMBED_MODEL, INTERNAL_EMBED_URL
|
||||
@@ -22,6 +23,23 @@ TOKEN_PATTERN = re.compile(r"[a-z0-9]+")
|
||||
class EmbedResult:
|
||||
vectors: list[list[float]]
|
||||
backend: str
|
||||
latency_ms: int = 0
|
||||
cache_hits: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class EmbeddingCache:
|
||||
store: dict[str, list[float]] = field(default_factory=dict)
|
||||
|
||||
def key(self, text: str) -> str:
|
||||
return hashlib.sha1((text or "").encode("utf-8")).hexdigest()
|
||||
|
||||
def get(self, text: str) -> list[float] | None:
|
||||
return self.store.get(self.key(text))
|
||||
|
||||
def put(self, text: str, vector: list[float]) -> None:
|
||||
if vector:
|
||||
self.store[self.key(text)] = vector
|
||||
|
||||
|
||||
def _local_vector(text: str) -> list[float]:
|
||||
@@ -41,19 +59,51 @@ def _local_vector(text: str) -> list[float]:
|
||||
|
||||
|
||||
def local_embed(texts: list[str]) -> EmbedResult:
|
||||
return EmbedResult(vectors=[_local_vector(text) for text in texts], backend="local")
|
||||
start = time.monotonic()
|
||||
vectors = [_local_vector(text) for text in texts]
|
||||
latency_ms = int((time.monotonic() - start) * 1000)
|
||||
return EmbedResult(vectors=vectors, backend="local", latency_ms=latency_ms)
|
||||
|
||||
|
||||
async def embed_texts(
|
||||
texts: list[str], api_key: str, *, gateway_url: str = INTERNAL_EMBED_URL
|
||||
texts: list[str],
|
||||
api_key: str,
|
||||
*,
|
||||
gateway_url: str = INTERNAL_EMBED_URL,
|
||||
cache: EmbeddingCache | None = None,
|
||||
) -> EmbedResult:
|
||||
if not texts:
|
||||
return EmbedResult(vectors=[], backend="empty")
|
||||
cache_hits = 0
|
||||
pending_index: list[int] = []
|
||||
pending_text: list[str] = []
|
||||
resolved: list[list[float] | None] = [None] * len(texts)
|
||||
if cache is not None:
|
||||
for index, text in enumerate(texts):
|
||||
cached_vector = cache.get(text)
|
||||
if cached_vector is not None:
|
||||
resolved[index] = cached_vector
|
||||
cache_hits += 1
|
||||
else:
|
||||
pending_index.append(index)
|
||||
pending_text.append(text)
|
||||
else:
|
||||
pending_index = list(range(len(texts)))
|
||||
pending_text = list(texts)
|
||||
if not pending_text:
|
||||
return EmbedResult(
|
||||
vectors=[vector or [] for vector in resolved],
|
||||
backend="cache",
|
||||
latency_ms=0,
|
||||
cache_hits=cache_hits,
|
||||
)
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {"model": INTERNAL_EMBED_MODEL, "input": texts}
|
||||
payload = {"model": INTERNAL_EMBED_MODEL, "input": pending_text}
|
||||
start = time.monotonic()
|
||||
backend = "gateway"
|
||||
try:
|
||||
async with stealth.stealth_async_client(timeout=EMBED_TIMEOUT_SECONDS) as client:
|
||||
response = await client.post(gateway_url, json=payload, headers=headers)
|
||||
@@ -61,10 +111,22 @@ async def embed_texts(
|
||||
raise RuntimeError(f"embed gateway returned {response.status_code}")
|
||||
data = response.json()
|
||||
rows = data.get("data") or []
|
||||
vectors = [row.get("embedding") or [] for row in rows]
|
||||
if len(vectors) != len(texts) or any(not vector for vector in vectors):
|
||||
fresh = [row.get("embedding") or [] for row in rows]
|
||||
if len(fresh) != len(pending_text) or any(not vector for vector in fresh):
|
||||
raise RuntimeError("embed gateway returned an incomplete response")
|
||||
return EmbedResult(vectors=vectors, backend="gateway")
|
||||
except Exception as exc:
|
||||
logger.warning("deepsearch embedding gateway failed, using local: %s", exc)
|
||||
return local_embed(texts)
|
||||
fresh = [_local_vector(text) for text in pending_text]
|
||||
backend = "local"
|
||||
latency_ms = int((time.monotonic() - start) * 1000)
|
||||
for offset, index in enumerate(pending_index):
|
||||
vector = fresh[offset]
|
||||
resolved[index] = vector
|
||||
if cache is not None:
|
||||
cache.put(pending_text[offset], vector)
|
||||
return EmbedResult(
|
||||
vectors=[vector or [] for vector in resolved],
|
||||
backend=backend,
|
||||
latency_ms=latency_ms,
|
||||
cache_hits=cache_hits,
|
||||
)
|
||||
|
||||
@@ -15,8 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
TOKEN_PATTERN = re.compile(r"[a-z0-9]+")
|
||||
BM25_K1 = 1.5
|
||||
BM25_B = 0.75
|
||||
HYBRID_VECTOR_WEIGHT = 0.6
|
||||
HYBRID_KEYWORD_WEIGHT = 0.4
|
||||
RRF_K = 60.0
|
||||
DEFAULT_TOP_K = 8
|
||||
CANDIDATE_MULTIPLIER = 4
|
||||
|
||||
@@ -182,18 +181,39 @@ class VectorStore:
|
||||
)
|
||||
if not candidates:
|
||||
return []
|
||||
vector_rank = sorted(
|
||||
candidates, key=lambda chunk: chunk.score, reverse=True
|
||||
)
|
||||
keyword = self.keyword_scores(query, candidates)
|
||||
vec_max = max((chunk.score for chunk in candidates), default=0.0) or 1.0
|
||||
kw_max = max(keyword.values(), default=0.0) or 1.0
|
||||
keyword_rank = sorted(
|
||||
candidates, key=lambda chunk: keyword.get(chunk.uid, 0.0), reverse=True
|
||||
)
|
||||
fused: dict[str, float] = {}
|
||||
for rank, chunk in enumerate(vector_rank, start=1):
|
||||
fused[chunk.uid] = fused.get(chunk.uid, 0.0) + 1.0 / (RRF_K + rank)
|
||||
for rank, chunk in enumerate(keyword_rank, start=1):
|
||||
fused[chunk.uid] = fused.get(chunk.uid, 0.0) + 1.0 / (RRF_K + rank)
|
||||
for chunk in candidates:
|
||||
vec_norm = max(0.0, chunk.score) / vec_max
|
||||
kw_norm = keyword.get(chunk.uid, 0.0) / kw_max
|
||||
chunk.score = (
|
||||
HYBRID_VECTOR_WEIGHT * vec_norm + HYBRID_KEYWORD_WEIGHT * kw_norm
|
||||
)
|
||||
chunk.score = fused.get(chunk.uid, 0.0)
|
||||
candidates.sort(key=lambda chunk: chunk.score, reverse=True)
|
||||
return candidates[:top_k]
|
||||
|
||||
def coverage_analytics(self) -> dict:
|
||||
chunks = self.all_chunks()
|
||||
if not chunks:
|
||||
return {"chunks": 0, "domains": 0, "sources": 0, "avg_chunk_chars": 0}
|
||||
domains = {chunk.metadata.get("url", "") for chunk in chunks}
|
||||
domains.discard("")
|
||||
sources = {chunk.source for chunk in chunks}
|
||||
sources.discard("")
|
||||
avg_chars = int(sum(len(chunk.text) for chunk in chunks) / len(chunks))
|
||||
return {
|
||||
"chunks": len(chunks),
|
||||
"domains": len(domains),
|
||||
"sources": len(sources),
|
||||
"avg_chunk_chars": avg_chars,
|
||||
}
|
||||
|
||||
def drop(self) -> None:
|
||||
try:
|
||||
import chromadb
|
||||
|
||||
Reference in New Issue
Block a user