Skip to content

Commit c545e3f

Browse files
feat(v1): dual-channel groundwork
- Add pluggable embeddings providers (hash fallback, ST, OpenAI) - Add in-memory cosine vector index and session TTL store - Add summarizers (heuristic, OpenAI) - Add v1 endpoints: embed, store, search, checkpoint, restore - Add openapi/openapi-v1.yaml - Update Python/TS clients with v1 helpers - Update README for v1 overview Note: follow-up will address remaining lint (line length).
1 parent ae86695 commit c545e3f

File tree

13 files changed

+869
-4
lines changed

13 files changed

+869
-4
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,23 @@ uvicorn src.contextforge_memory.main:app --host 0.0.0.0 --port 8085
2424
- `POST /v0/search`
2525
- `POST /v0/embed`
2626

27+
### v1 (enabled by default; set `CF_ENABLE_V1=false` to disable)
28+
29+
- `POST /v1/embed` (provider-backed)
30+
- `POST /v1/store` (accepts optional `vectors`, updates vector index)
31+
- `POST /v1/search` (vector search)
32+
- `POST /v1/checkpoint` (ephemeral session checkpoint)
33+
- `POST /v1/restore` (stitched context: recent checkpoints + relevant memory)
34+
2735
See `openapi/openapi-v0.yaml` for schemas.
2836

2937
## Clients
3038

3139
- Python: `clients/python/contextforge_client.py`
3240
- TypeScript: `clients/typescript/contextforgeClient.ts`
3341

42+
Both clients now expose v1 helpers (prefixed with `v1*`).
43+
3444
## CI
3545

3646
Basic import smoke in GitHub Actions (see `.github/workflows/ci.yml`).

clients/python/contextforge_client.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55

66

77
class ContextForgeClient:
8-
def __init__(self, base_url: str) -> None:
8+
def __init__(self, base_url: str, api_key: str | None = None) -> None:
99
self.base_url = base_url.rstrip("/")
1010
self._client = httpx.Client(timeout=15.0)
11+
self._headers = {"x-api-key": api_key} if api_key else {}
1112

1213
def health(self) -> Dict[str, Any]:
1314
r = self._client.get(f"{self.base_url}/v0/health")
@@ -40,3 +41,72 @@ def embed(self, texts: List[str]) -> Dict[str, Any]:
4041
)
4142
r.raise_for_status()
4243
return r.json()
44+
45+
# v1 API
46+
def v1_store(
47+
self, items: List[Dict[str, Any]], vectors: List[List[float]] | None = None
48+
) -> Dict[str, Any]:
49+
payload: Dict[str, Any] = {"items": items}
50+
if vectors is not None:
51+
payload["vectors"] = vectors
52+
r = self._client.post(
53+
f"{self.base_url}/v1/store",
54+
json=payload,
55+
headers=self._headers,
56+
)
57+
r.raise_for_status()
58+
return r.json()
59+
60+
def v1_search(
61+
self, namespace: str, project_id: str, query: str, top_k: int = 5
62+
) -> Dict[str, Any]:
63+
payload = {
64+
"namespace": namespace,
65+
"project_id": project_id,
66+
"query": query,
67+
"top_k": top_k,
68+
}
69+
r = self._client.post(
70+
f"{self.base_url}/v1/search",
71+
json=payload,
72+
headers=self._headers,
73+
)
74+
r.raise_for_status()
75+
return r.json()
76+
77+
def v1_embed(self, texts: List[str]) -> Dict[str, Any]:
78+
r = self._client.post(
79+
f"{self.base_url}/v1/embed",
80+
json={"texts": texts},
81+
headers=self._headers,
82+
)
83+
r.raise_for_status()
84+
return r.json()
85+
86+
def v1_checkpoint(
87+
self,
88+
session_id: str,
89+
phase: str,
90+
summary: str | None = None,
91+
metadata: Dict[str, Any] | None = None,
92+
) -> Dict[str, Any]:
93+
payload: Dict[str, Any] = {"session_id": session_id, "phase": phase}
94+
if summary is not None:
95+
payload["summary"] = summary
96+
if metadata is not None:
97+
payload["metadata"] = metadata
98+
r = self._client.post(
99+
f"{self.base_url}/v1/checkpoint", json=payload, headers=self._headers
100+
)
101+
r.raise_for_status()
102+
return r.json()
103+
104+
def v1_restore(self, session_id: str, task: str, top_k: int = 5) -> Dict[str, Any]:
105+
payload = {"session_id": session_id, "task": task, "top_k": top_k}
106+
r = self._client.post(
107+
f"{self.base_url}/v1/restore",
108+
json=payload,
109+
headers=self._headers,
110+
)
111+
r.raise_for_status()
112+
return r.json()

clients/typescript/contextforgeClient.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ export type MemoryItem = {
99

1010
export class ContextForgeClient {
1111
private baseUrl: string;
12+
private apiKey?: string;
1213

13-
constructor(baseUrl: string) {
14+
constructor(baseUrl: string, apiKey?: string) {
1415
this.baseUrl = baseUrl.replace(/\/$/, "");
16+
this.apiKey = apiKey;
1517
}
1618

1719
async health(): Promise<{ status: string }> {
@@ -49,6 +51,68 @@ export class ContextForgeClient {
4951
if (!r.ok) throw new Error(`embed failed: ${r.status}`);
5052
return r.json();
5153
}
54+
55+
// v1 API
56+
private authHeaders(): Record<string, string> {
57+
const h: Record<string, string> = { "Content-Type": "application/json" };
58+
if (this.apiKey) h["x-api-key"] = this.apiKey;
59+
return h;
60+
}
61+
62+
async v1Store(items: MemoryItem[], vectors?: number[][]): Promise<{ stored: number }> {
63+
const body: any = { items };
64+
if (vectors) body.vectors = vectors;
65+
const r = await fetch(`${this.baseUrl}/v1/store`, {
66+
method: "POST",
67+
headers: this.authHeaders(),
68+
body: JSON.stringify(body),
69+
});
70+
if (!r.ok) throw new Error(`v1 store failed: ${r.status}`);
71+
return r.json();
72+
}
73+
74+
async v1Search(namespace: string, project_id: string, query: string, top_k = 5): Promise<{ results: MemoryItem[] }> {
75+
const r = await fetch(`${this.baseUrl}/v1/search`, {
76+
method: "POST",
77+
headers: this.authHeaders(),
78+
body: JSON.stringify({ namespace, project_id, query, top_k }),
79+
});
80+
if (!r.ok) throw new Error(`v1 search failed: ${r.status}`);
81+
return r.json();
82+
}
83+
84+
async v1Embed(texts: string[]): Promise<{ vectors: number[][] }> {
85+
const r = await fetch(`${this.baseUrl}/v1/embed`, {
86+
method: "POST",
87+
headers: this.authHeaders(),
88+
body: JSON.stringify({ texts }),
89+
});
90+
if (!r.ok) throw new Error(`v1 embed failed: ${r.status}`);
91+
return r.json();
92+
}
93+
94+
async v1Checkpoint(session_id: string, phase: string, summary?: string, metadata?: Record<string, unknown>): Promise<{ ok: boolean }> {
95+
const body: any = { session_id, phase };
96+
if (summary) body.summary = summary;
97+
if (metadata) body.metadata = metadata;
98+
const r = await fetch(`${this.baseUrl}/v1/checkpoint`, {
99+
method: "POST",
100+
headers: this.authHeaders(),
101+
body: JSON.stringify(body),
102+
});
103+
if (!r.ok) throw new Error(`v1 checkpoint failed: ${r.status}`);
104+
return r.json();
105+
}
106+
107+
async v1Restore(session_id: string, task: string, top_k = 5): Promise<{ context: string }> {
108+
const r = await fetch(`${this.baseUrl}/v1/restore`, {
109+
method: "POST",
110+
headers: this.authHeaders(),
111+
body: JSON.stringify({ session_id, task, top_k }),
112+
});
113+
if (!r.ok) throw new Error(`v1 restore failed: ${r.status}`);
114+
return r.json();
115+
}
52116
}
53117

54118

0 commit comments

Comments
 (0)