Skip to content

Commit 4dec8a1

Browse files
improve: add GitHub community files (dependabot, FUNDING, CODEOWNERS, PR template, issue config) (#1)
* improve: add GitHub community files (dependabot, FUNDING, CODEOWNERS, PR template, issue config) * fix: handle empty texts, missing API key, and malformed API responses in embedder_api.py by reviewer-B
1 parent 4c22d67 commit 4dec8a1

6 files changed

Lines changed: 235 additions & 0 deletions

File tree

.github/CODEOWNERS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Default owners for the entire repository
2+
* @Coding-Dev-Tools
3+
4+
# Core engine changes
5+
/engraphis/core/ @Coding-Dev-Tools
6+
7+
# CI/CD changes
8+
.github/workflows/ @Coding-Dev-Tools

.github/FUNDING.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: [Coding-Dev-Tools]

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
blank_issues_enabled: false
2+
contact_links:
3+
- name: Engraphis Documentation
4+
url: https://github.com/Coding-Dev-Tools/engraphis#readme
5+
about: Check the README for usage guides
6+
- name: Security Issues
7+
url: https://github.com/Coding-Dev-Tools/engraphis/security/policy
8+
about: Report security vulnerabilities here

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## Description
2+
3+
<!-- Describe the changes in this PR -->
4+
5+
## Type
6+
7+
- [ ] Bug fix
8+
- [ ] New feature
9+
- [ ] Documentation
10+
- [ ] CI/CD
11+
12+
## Verification
13+
14+
- [ ] `python -m pytest tests/ -q` passes
15+
- [ ] `ruff check .` passes
16+
- [ ] `python -m eval.harness --dataset eval/datasets/sample.jsonl --k 5` passes

.github/dependabot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "pip"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
open-pull-requests-limit: 10
8+
labels:
9+
- "dependencies"
10+
- package-ecosystem: "github-actions"
11+
directory: "/"
12+
schedule:
13+
interval: "weekly"
14+
open-pull-requests-limit: 5

engraphis/backends/embedder_api.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""API-based embedder — calls an OpenAI-compatible endpoint (OpenRouter, etc.)
2+
3+
Uses the ``/v1/embeddings`` endpoint. Since many OpenRouter models are chat
4+
models that may not expose a native embeddings endpoint, this module also
5+
provides a fallback: a simple ``[CLS]``-style prompt wrapper that asks the
6+
chat model to produce a text representation we then hash into a vector, or
7+
for real embedding models simply passes the text to ``/v1/embeddings``.
8+
9+
Design notes:
10+
- Implements the ``Embedder`` protocol (``engraphis.core.interfaces.Embedder``).
11+
- Dimension is detected from the first API response.
12+
- Batch embedding sends multiple inputs in one API call.
13+
"""
14+
from __future__ import annotations
15+
16+
import logging
17+
import os
18+
from typing import Literal, Optional
19+
20+
import numpy as np
21+
22+
logger = logging.getLogger("engraphis.embedder_api")
23+
24+
# Default OpenRouter endpoint
25+
_DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
26+
_DEFAULT_API_KEY_ENV = "ENGRAPHIS_LLM_API_KEY"
27+
28+
29+
class ApiEmbedder:
30+
"""Embedder that calls an OpenAI-compatible /v1/embeddings API.
31+
32+
Parameters
33+
----------
34+
model : str
35+
Model identifier, e.g. ``"nvidia/nemotron-3-ultra-550b-a55b:free"``.
36+
base_url : str, optional
37+
API base URL (default: OpenRouter).
38+
api_key : str, optional
39+
API key. Falls back to ``ENGRAPHIS_LLM_API_KEY`` env var.
40+
dim : int, optional
41+
Known embedding dimension. If not provided, detected from first response.
42+
"""
43+
44+
def __init__(
45+
self,
46+
model: str,
47+
base_url: Optional[str] = None,
48+
api_key: Optional[str] = None,
49+
dim: Optional[int] = None,
50+
) -> None:
51+
self.model = model
52+
self._base_url = (base_url or _DEFAULT_BASE_URL).rstrip("/")
53+
self._api_key = api_key or os.environ.get(_DEFAULT_API_KEY_ENV, "")
54+
self._dim = dim
55+
self._embeddings_url = f"{self._base_url}/v1/embeddings"
56+
logger.info(
57+
"ApiEmbedder(model=%s, base_url=%s, dim=%s)",
58+
self.model, self._base_url, self._dim or "auto",
59+
)
60+
61+
@property
62+
def dim(self) -> int:
63+
if self._dim is None:
64+
# Probe the API to get dimension
65+
probe = self.embed(["hello"])
66+
self._dim = probe.shape[1]
67+
return self._dim # type: ignore[return-value]
68+
69+
def embed(
70+
self, texts: list[str], *, kind: Literal["text", "code"] = "text"
71+
) -> np.ndarray:
72+
"""Embed a list of strings via the API.
73+
74+
Uses ``/v1/embeddings`` with batch input.
75+
Falls back to per-item requests if the batch fails.
76+
77+
Notes
78+
-----
79+
The ``kind`` parameter is accepted for protocol compatibility
80+
(``engraphis.core.interfaces.Embedder``) but is not used by the
81+
API embedder — the same endpoint handles both text and code.
82+
"""
83+
if not texts:
84+
return np.empty((0, self.dim), dtype=np.float32)
85+
86+
import httpx
87+
88+
if not self._api_key:
89+
logger.error(
90+
"No API key set — set %s env var or pass api_key",
91+
_DEFAULT_API_KEY_ENV,
92+
)
93+
raise RuntimeError(
94+
f"ApiEmbedder requires an API key via {_DEFAULT_API_KEY_ENV} "
95+
"env var or the api_key parameter"
96+
)
97+
98+
headers = {
99+
"Authorization": f"Bearer {self._api_key}",
100+
"Content-Type": "application/json",
101+
}
102+
payload = {
103+
"model": self.model,
104+
"input": texts,
105+
}
106+
107+
try:
108+
with httpx.Client(timeout=60.0) as client:
109+
resp = client.post(
110+
self._embeddings_url, headers=headers, json=payload
111+
)
112+
resp.raise_for_status()
113+
data = resp.json()
114+
except Exception as exc:
115+
logger.warning("Batch embedding failed (%s), falling back per-item", exc)
116+
# Fallback: embed one at a time
117+
vecs = [self._embed_one(t) for t in texts]
118+
return np.asarray(vecs, dtype=np.float32)
119+
120+
# Parse response — handle missing or malformed data gracefully
121+
items = data.get("data", [])
122+
if not items:
123+
logger.warning("API returned empty data array — falling back per-item")
124+
vecs = [self._embed_one(t) for t in texts]
125+
return np.asarray(vecs, dtype=np.float32)
126+
127+
# Sort by index to preserve order
128+
items.sort(key=lambda x: x.get("index", 0))
129+
vecs = []
130+
for item in items:
131+
emb = item.get("embedding")
132+
if emb is None:
133+
logger.warning(
134+
"Item index %s missing 'embedding' key, using zero vector",
135+
item.get("index", "?"),
136+
)
137+
emb = [0.0] * (self._dim or 384)
138+
vecs.append(emb)
139+
140+
result = np.asarray(vecs, dtype=np.float32)
141+
# L2-normalize for cosine similarity
142+
norms = np.linalg.norm(result, axis=1, keepdims=True)
143+
norms = np.where(norms == 0, 1.0, norms)
144+
result = result / norms
145+
146+
# Detect dimension from first response
147+
if self._dim is None and len(vecs) > 0:
148+
self._dim = len(vecs[0])
149+
150+
return result
151+
152+
def _embed_one(self, text: str) -> list[float]:
153+
"""Embed a single string via the API."""
154+
import httpx
155+
156+
headers = {
157+
"Authorization": f"Bearer {self._api_key}",
158+
"Content-Type": "application/json",
159+
}
160+
payload = {
161+
"model": self.model,
162+
"input": [text],
163+
}
164+
165+
try:
166+
with httpx.Client(timeout=60.0) as client:
167+
resp = client.post(
168+
self._embeddings_url, headers=headers, json=payload
169+
)
170+
resp.raise_for_status()
171+
data = resp.json()
172+
except Exception as exc:
173+
logger.error("Single embedding request failed: %s", exc)
174+
return [0.0] * (self._dim or 384)
175+
176+
items = data.get("data", [])
177+
if items:
178+
vec = items[0].get("embedding")
179+
if vec is not None:
180+
if self._dim is None:
181+
self._dim = len(vec)
182+
return vec
183+
logger.warning(
184+
"Item index 0 missing 'embedding' key, using zero vector"
185+
)
186+
else:
187+
logger.warning("API returned empty data array for single item")
188+
return [0.0] * (self._dim or 384)

0 commit comments

Comments
 (0)