winnex-ai-normalize 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ Business Source License 1.1
2
+
3
+ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
4
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
5
+
6
+ Parameters
7
+
8
+ Licensor: Winnex AI
9
+ Licensed Work: Tracer-GOV
10
+ Additional Use Grant: Free for Brazilian government agencies.
11
+ Any use by a public sector entity in Brazil is permitted.
12
+
13
+ Change Date: 2036-01-01
14
+ Change License: GNU General Public License v2.0 or later
15
+
16
+ For inquiries: pay@winnex.ai
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: winnex-ai-normalize
3
+ Version: 1.0.0
4
+ Summary: Input normalization plug for the Madhava engine — embed text via any provider (OpenAI, Qwen3, nano, xfactor), validate/normalize to float32/uint8, with provider failover and NO fake-vector fallback. Consumed by winnex-madhava, winnex-tracer, Liferay and the Maestro.
5
+ Author-email: Winnex Brasil Soluções Empresariais LTDA-ME <pay@winnex.ai>
6
+ License: Business Source License 1.1
7
+
8
+ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
9
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
10
+
11
+ Parameters
12
+
13
+ Licensor: Winnex AI
14
+ Licensed Work: Tracer-GOV
15
+ Additional Use Grant: Free for Brazilian government agencies.
16
+ Any use by a public sector entity in Brazil is permitted.
17
+
18
+ Change Date: 2036-01-01
19
+ Change License: GNU General Public License v2.0 or later
20
+
21
+ For inquiries: pay@winnex.ai
22
+
23
+ Project-URL: Homepage, https://winnex.ai
24
+ Project-URL: Source, https://github.com/winnex-ai/winnex-ai-normalize
25
+ Keywords: embedding,normalize,madhava,vector,openai,qwen3,rag,semantic-search,normalizer,winnex,failover
26
+ Classifier: Development Status :: 4 - Beta
27
+ Classifier: Intended Audience :: Developers
28
+ Classifier: License :: Other/Proprietary License
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
31
+ Requires-Python: >=3.10
32
+ Description-Content-Type: text/markdown
33
+ License-File: LICENSE
34
+ Requires-Dist: numpy>=1.24
35
+ Requires-Dist: httpx>=0.24
36
+ Provides-Extra: api
37
+ Requires-Dist: fastapi>=0.100; extra == "api"
38
+ Requires-Dist: uvicorn>=0.20; extra == "api"
39
+ Provides-Extra: madhava
40
+ Requires-Dist: winnex-madhava>=1.9.2; extra == "madhava"
41
+ Provides-Extra: all
42
+ Requires-Dist: fastapi>=0.100; extra == "all"
43
+ Requires-Dist: uvicorn>=0.20; extra == "all"
44
+ Requires-Dist: winnex-madhava>=1.9.2; extra == "all"
45
+ Dynamic: license-file
46
+
47
+ # winnex-ai-normalize
48
+
49
+ **The input-normalization plug for the Madhava engine.**
50
+
51
+ The Madhava engine is **agnostic** — it consumes float32 vectors. This
52
+ package normalizes **any** input (text via an embedding provider, or raw
53
+ vectors) into a valid float32/uint8 corpus ready for
54
+ `winnex_madhava.build_engine`, with **provider failover** and **no silent
55
+ fallback to fake vectors**.
56
+
57
+ ```
58
+ text / raw vectors
59
+
60
+
61
+ winnex-ai-normalize
62
+ ├── providers: OpenAI / Qwen3 / nano / xfactor / direct (failover order)
63
+ ├── validate: dim + NaN + norm (fail loudly)
64
+ ├── normalize: L2 unit-norm (cosine contract)
65
+ └── quantize: float32 → uint8 (the engine's native corpus)
66
+
67
+
68
+ winnex-madhava (agnostic) → top-K + Cauchy-Schwarz proof
69
+ ```
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install winnex-ai-normalize # core (numpy + httpx)
75
+ pip install winnex-ai-normalize[api] # + OpenAI-compatible /v1/embeddings
76
+ pip install winnex-ai-normalize[all] # + winnex-madhava
77
+ ```
78
+
79
+ ## Quick start
80
+
81
+ ```python
82
+ from winnex_ai_normalize import EmbeddingNormalizer
83
+
84
+ norm = EmbeddingNormalizer() # reads env / JSON config
85
+
86
+ # 1. Embed text via the configured provider (OpenAI / Qwen3 / ...)
87
+ vecs = norm.vectorize_texts(["hypertension treatment", "diabetes care"])
88
+ # vecs: (2, d) float32 L2-normalized
89
+
90
+ # 2. Or normalize raw vectors (validated, no NaN, unit-norm)
91
+ vecs = norm.normalize_vectors(my_embeddings, dim=1024)
92
+
93
+ # 3. Feed the Madhava engine (agnostic)
94
+ u8 = norm.to_corpus(vecs, dim=1024) # uint8 corpus for build_engine
95
+ import winnex_madhava as wm
96
+ engine = wm.build_engine(u8, dim=1024, metric="cosine", k=10)
97
+ ```
98
+
99
+ ## OpenAI-compatible API
100
+
101
+ ```bash
102
+ uvicorn winnex_ai_normalize.api.server:app --port 8102
103
+ curl -X POST http://localhost:8102/v1/embeddings \
104
+ -H "Content-Type: application/json" \
105
+ -d '{"model": "Qwen3-Embedding-0.6B", "input": ["text one", "text two"]}'
106
+ # → {"data": [{"embedding": [...], "index": 0}, ...], ...}
107
+ ```
108
+
109
+ ## Provider failover (no fake fallback)
110
+
111
+ Providers are tried in `provider_order` (JSON/env config). If **all**
112
+ fail, a `RuntimeError` is raised with the collected errors — a normalizer
113
+ must never fabricate embeddings.
114
+
115
+ ```json
116
+ {
117
+ "default_dim": 1024,
118
+ "default_provider": "qwen3",
119
+ "providers": [
120
+ {"name": "qwen3", "base_url": "http://winnex-embedding:8102",
121
+ "model": "/workspace/models/Qwen3-Embedding-0.6B", "priority": 1},
122
+ {"name": "openai", "base_url": "https://api.openai.com/v1",
123
+ "model": "text-embedding-3-small", "api_key_env": "OPENAI_API_KEY", "priority": 2}
124
+ ],
125
+ "provider_order": ["qwen3", "openai"]
126
+ }
127
+ ```
128
+
129
+ ## Consumed by
130
+
131
+ - **winnex-madhava** (direct — the engine stays agnostic)
132
+ - **winnex-tracer** (audit + commitment)
133
+ - **Liferay bridges** (tracer-gov-liferay, tracer-med-liferay)
134
+ - **Maestro** (winnex-ai-server / winnex-ai-engine) and any external tool
135
+
136
+ ## License
137
+
138
+ Business Source License 1.1 (BSL 1.1) | pay@winnex.ai |
139
+ Winnex Brasil Soluções Empresariais LTDA (CNPJ 58.364.637/0001-47)
@@ -0,0 +1,93 @@
1
+ # winnex-ai-normalize
2
+
3
+ **The input-normalization plug for the Madhava engine.**
4
+
5
+ The Madhava engine is **agnostic** — it consumes float32 vectors. This
6
+ package normalizes **any** input (text via an embedding provider, or raw
7
+ vectors) into a valid float32/uint8 corpus ready for
8
+ `winnex_madhava.build_engine`, with **provider failover** and **no silent
9
+ fallback to fake vectors**.
10
+
11
+ ```
12
+ text / raw vectors
13
+
14
+
15
+ winnex-ai-normalize
16
+ ├── providers: OpenAI / Qwen3 / nano / xfactor / direct (failover order)
17
+ ├── validate: dim + NaN + norm (fail loudly)
18
+ ├── normalize: L2 unit-norm (cosine contract)
19
+ └── quantize: float32 → uint8 (the engine's native corpus)
20
+
21
+
22
+ winnex-madhava (agnostic) → top-K + Cauchy-Schwarz proof
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install winnex-ai-normalize # core (numpy + httpx)
29
+ pip install winnex-ai-normalize[api] # + OpenAI-compatible /v1/embeddings
30
+ pip install winnex-ai-normalize[all] # + winnex-madhava
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ ```python
36
+ from winnex_ai_normalize import EmbeddingNormalizer
37
+
38
+ norm = EmbeddingNormalizer() # reads env / JSON config
39
+
40
+ # 1. Embed text via the configured provider (OpenAI / Qwen3 / ...)
41
+ vecs = norm.vectorize_texts(["hypertension treatment", "diabetes care"])
42
+ # vecs: (2, d) float32 L2-normalized
43
+
44
+ # 2. Or normalize raw vectors (validated, no NaN, unit-norm)
45
+ vecs = norm.normalize_vectors(my_embeddings, dim=1024)
46
+
47
+ # 3. Feed the Madhava engine (agnostic)
48
+ u8 = norm.to_corpus(vecs, dim=1024) # uint8 corpus for build_engine
49
+ import winnex_madhava as wm
50
+ engine = wm.build_engine(u8, dim=1024, metric="cosine", k=10)
51
+ ```
52
+
53
+ ## OpenAI-compatible API
54
+
55
+ ```bash
56
+ uvicorn winnex_ai_normalize.api.server:app --port 8102
57
+ curl -X POST http://localhost:8102/v1/embeddings \
58
+ -H "Content-Type: application/json" \
59
+ -d '{"model": "Qwen3-Embedding-0.6B", "input": ["text one", "text two"]}'
60
+ # → {"data": [{"embedding": [...], "index": 0}, ...], ...}
61
+ ```
62
+
63
+ ## Provider failover (no fake fallback)
64
+
65
+ Providers are tried in `provider_order` (JSON/env config). If **all**
66
+ fail, a `RuntimeError` is raised with the collected errors — a normalizer
67
+ must never fabricate embeddings.
68
+
69
+ ```json
70
+ {
71
+ "default_dim": 1024,
72
+ "default_provider": "qwen3",
73
+ "providers": [
74
+ {"name": "qwen3", "base_url": "http://winnex-embedding:8102",
75
+ "model": "/workspace/models/Qwen3-Embedding-0.6B", "priority": 1},
76
+ {"name": "openai", "base_url": "https://api.openai.com/v1",
77
+ "model": "text-embedding-3-small", "api_key_env": "OPENAI_API_KEY", "priority": 2}
78
+ ],
79
+ "provider_order": ["qwen3", "openai"]
80
+ }
81
+ ```
82
+
83
+ ## Consumed by
84
+
85
+ - **winnex-madhava** (direct — the engine stays agnostic)
86
+ - **winnex-tracer** (audit + commitment)
87
+ - **Liferay bridges** (tracer-gov-liferay, tracer-med-liferay)
88
+ - **Maestro** (winnex-ai-server / winnex-ai-engine) and any external tool
89
+
90
+ ## License
91
+
92
+ Business Source License 1.1 (BSL 1.1) | pay@winnex.ai |
93
+ Winnex Brasil Soluções Empresariais LTDA (CNPJ 58.364.637/0001-47)
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "winnex-ai-normalize"
7
+ version = "1.0.0"
8
+ description = "Input normalization plug for the Madhava engine — embed text via any provider (OpenAI, Qwen3, nano, xfactor), validate/normalize to float32/uint8, with provider failover and NO fake-vector fallback. Consumed by winnex-madhava, winnex-tracer, Liferay and the Maestro."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "Winnex Brasil Soluções Empresariais LTDA-ME", email = "pay@winnex.ai" },
14
+ ]
15
+ keywords = [
16
+ "embedding", "normalize", "madhava", "vector", "openai", "qwen3",
17
+ "rag", "semantic-search", "normalizer", "winnex", "failover",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Intended Audience :: Developers",
22
+ "License :: Other/Proprietary License",
23
+ "Programming Language :: Python :: 3",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ ]
26
+ dependencies = [
27
+ "numpy>=1.24",
28
+ "httpx>=0.24",
29
+ ]
30
+
31
+ [project.optional-dependencies]
32
+ api = ["fastapi>=0.100", "uvicorn>=0.20"]
33
+ madhava = ["winnex-madhava>=1.9.2"]
34
+ all = [
35
+ "fastapi>=0.100",
36
+ "uvicorn>=0.20",
37
+ "winnex-madhava>=1.9.2",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://winnex.ai"
42
+ Source = "https://github.com/winnex-ai/winnex-ai-normalize"
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["winnex_ai_normalize/tests"]
46
+
47
+ [tool.setuptools.packages.find]
48
+ include = ["winnex_ai_normalize*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,42 @@
1
+ """winnex-ai-normalize — the input-normalization plug for the Madhava engine.
2
+
3
+ The Madhava engine is AGNOSTIC: it consumes float32 vectors. This package
4
+ normalizes ANY input (text via an embedding provider, or raw vectors) into a
5
+ valid float32/uint8 corpus ready for `winnex_madhava.build_engine`, with
6
+ provider failover and NO silent fallback to fake vectors.
7
+
8
+ Components:
9
+ - core.config: JSON-driven provider/config (model, dim, failover order)
10
+ - core.embedding: EmbeddingService with provider failover + cache
11
+ - core.normalize: validate / L2-normalize / quantize → Madhava corpus
12
+ - api.server: OpenAI-compatible /v1/embeddings endpoint
13
+
14
+ Consumed by: winnex-madhava (direct), winnex-tracer, the Liferay bridges,
15
+ and the Maestro — any tool that needs to feed vectors to Madhava.
16
+
17
+ Business Source License 1.1 (BSL 1.1) | pay@winnex.ai
18
+ """
19
+ from .core.config import NormalizeConfig, ProviderConfig, load_config
20
+ from .core.embedding import EmbeddingProvider, EmbeddingService, get_embedding_service
21
+ from .core.normalize import (
22
+ EmbeddingNormalizer,
23
+ validate_embeddings,
24
+ normalize_l2,
25
+ quantize_corpus,
26
+ )
27
+
28
+ __version__ = "1.0.0"
29
+
30
+ __all__ = [
31
+ "NormalizeConfig",
32
+ "ProviderConfig",
33
+ "load_config",
34
+ "EmbeddingProvider",
35
+ "EmbeddingService",
36
+ "get_embedding_service",
37
+ "EmbeddingNormalizer",
38
+ "validate_embeddings",
39
+ "normalize_l2",
40
+ "quantize_corpus",
41
+ "__version__",
42
+ ]
@@ -0,0 +1,78 @@
1
+ """
2
+ winnex-ai-normalize — OpenAI-compatible embedding API.
3
+
4
+ Exposes the normalization service behind the OpenAI `/v1/embeddings`
5
+ contract, so any consumer (Liferay, Maestro, tracer, or a plain OpenAI
6
+ client) can get Madhava-ready vectors:
7
+
8
+ POST /v1/embeddings {"model": "...", "input": ["text", ...]}
9
+ → {"data": [{"embedding": [..], "index": 0}], "model": "...", "usage": {...}}
10
+
11
+ Also exposes:
12
+ GET /v1/health — provider availability
13
+ GET /v1/normalize/health — alias (Liferay-friendly)
14
+
15
+ License: Business Source License 1.1 (BSL 1.1)
16
+ """
17
+ import os
18
+ from typing import List, Optional
19
+
20
+ from fastapi import FastAPI, HTTPException
21
+ from pydantic import BaseModel
22
+
23
+ from winnex_ai_normalize.core.embedding import get_embedding_service
24
+ from winnex_ai_normalize.core.config import load_config
25
+
26
+ app = FastAPI(
27
+ title="winnex-ai-normalize",
28
+ version="1.0.0",
29
+ description="Input normalization for the Madhava engine (OpenAI-compatible embeddings).",
30
+ )
31
+
32
+
33
+ class EmbeddingsRequest(BaseModel):
34
+ model: str = ""
35
+ input: List[str]
36
+
37
+
38
+ @app.post("/v1/embeddings")
39
+ def embeddings(req: EmbeddingsRequest):
40
+ """OpenAI-compatible embeddings endpoint (Madhava-ready vectors)."""
41
+ service = get_embedding_service()
42
+ try:
43
+ vecs = service.embed_texts(req.input)
44
+ except RuntimeError as e:
45
+ raise HTTPException(503, str(e))
46
+ data = [
47
+ {"embedding": vecs[i].tolist(), "index": i}
48
+ for i in range(len(vecs))
49
+ ]
50
+ return {
51
+ "object": "list",
52
+ "data": data,
53
+ "model": req.model or "winnex-ai-normalize",
54
+ "usage": {"prompt_tokens": sum(len(t) // 4 for t in req.input),
55
+ "total_tokens": sum(len(t) // 4 for t in req.input)},
56
+ }
57
+
58
+
59
+ @app.get("/v1/health")
60
+ @app.get("/v1/normalize/health")
61
+ def health():
62
+ """Provider availability (fail loudly if none reachable)."""
63
+ cfg = load_config()
64
+ svc = get_embedding_service()
65
+ status = svc.check_available()
66
+ return {
67
+ "status": "ok" if status.get("available") else "degraded",
68
+ "service": "winnex-ai-normalize",
69
+ "default_dim": cfg.default_dim,
70
+ "default_provider": cfg.default_provider,
71
+ "provider_order": cfg.provider_order,
72
+ "providers": status,
73
+ }
74
+
75
+
76
+ if __name__ == "__main__":
77
+ import uvicorn
78
+ uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8102")))
@@ -0,0 +1 @@
1
+ """winnex-ai-normalize — core (config, embedding providers, normalization)."""
@@ -0,0 +1,97 @@
1
+ """
2
+ winnex-ai-normalize — configuration (JSON-driven).
3
+
4
+ The single source of truth for the normalization providers, the embedding
5
+ model and the provider priority/failover order. Mirrors the Maestro's
6
+ `ai_models_config` (JSON-driven, no hardcoded values).
7
+
8
+ Config sources, in order of precedence:
9
+ 1. Constructor / env vars (WINNEX_AI_NORMALIZE_*)
10
+ 2. A JSON file path (WINNEX_AI_NORMALIZE_CONFIG)
11
+ 3. Sensible defaults.
12
+
13
+ License: Business Source License 1.1 (BSL 1.1)
14
+ """
15
+ import json
16
+ import os
17
+ from dataclasses import dataclass, field
18
+ from typing import Any, Dict, List, Optional
19
+
20
+
21
+ @dataclass
22
+ class ProviderConfig:
23
+ """A normalization provider (embedding source)."""
24
+ name: str # "openai" | "qwen3" | "nano" | "xfactor" | "direct"
25
+ type: str = "openai_compat" # how to call it: openai_compat | local | direct
26
+ model: str = ""
27
+ base_url: str = ""
28
+ api_key_env: str = "" # env var holding the API key (never hardcoded)
29
+ dim: int = 0 # 0 = auto-detect
30
+ timeout: float = 20.0
31
+ priority: int = 10 # lower = tried first (failover order)
32
+ enabled: bool = True
33
+
34
+
35
+ @dataclass
36
+ class NormalizeConfig:
37
+ """Top-level normalization configuration."""
38
+ default_dim: int = 1024 # Qwen3-Embedding-0.6B
39
+ default_provider: str = "qwen3" # the first provider to try
40
+ normalize_l2: bool = True # L2-normalize embeddings (cosine contract)
41
+ validate_nan: bool = True # fail loudly on NaN/inf
42
+ validate_range: bool = False # warn (not fail) on out-of-range
43
+ cache_max: int = 512 # bounded embedding cache
44
+ providers: List[ProviderConfig] = field(default_factory=list)
45
+ provider_order: List[str] = field(default_factory=list) # failover order
46
+
47
+ @classmethod
48
+ def from_env(cls) -> "NormalizeConfig":
49
+ """Load from WINNEX_AI_NORMALIZE_* env vars / JSON file."""
50
+ cfg = cls()
51
+ cfg.default_dim = int(os.environ.get("WINNEX_AI_NORMALIZE_DIM", cfg.default_dim))
52
+ cfg.default_provider = os.environ.get(
53
+ "WINNEX_AI_NORMALIZE_PROVIDER", cfg.default_provider)
54
+ # A config JSON may define the providers + failover order.
55
+ json_path = os.environ.get("WINNEX_AI_NORMALIZE_CONFIG", "")
56
+ if json_path and os.path.exists(json_path):
57
+ with open(json_path) as f:
58
+ data = json.load(f)
59
+ cfg.default_dim = int(data.get("default_dim", cfg.default_dim))
60
+ cfg.default_provider = data.get("default_provider", cfg.default_provider)
61
+ cfg.normalize_l2 = bool(data.get("normalize_l2", cfg.normalize_l2))
62
+ cfg.cache_max = int(data.get("cache_max", cfg.cache_max))
63
+ for p in data.get("providers", []):
64
+ cfg.providers.append(ProviderConfig(**p))
65
+ cfg.provider_order = list(data.get("provider_order", []))
66
+ # Default providers if none configured.
67
+ if not cfg.providers:
68
+ cfg.providers = [
69
+ ProviderConfig(
70
+ name="qwen3", type="openai_compat",
71
+ model=os.environ.get("EMBEDDING_MODEL", "/workspace/models/Qwen3-Embedding-0.6B"),
72
+ base_url=os.environ.get("EMBEDDING_URL", "http://winnex-embedding:8102"),
73
+ dim=cfg.default_dim, priority=1),
74
+ ProviderConfig(
75
+ name="openai", type="openai_compat",
76
+ model=os.environ.get("OPENAI_EMBEDDING_MODEL", "text-embedding-3-small"),
77
+ base_url=os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"),
78
+ api_key_env="OPENAI_API_KEY", priority=2),
79
+ ProviderConfig(
80
+ name="direct", type="direct",
81
+ dim=cfg.default_dim, priority=99),
82
+ ]
83
+ if not cfg.provider_order:
84
+ cfg.provider_order = [p.name for p in sorted(
85
+ cfg.providers, key=lambda p: p.priority)]
86
+ return cfg
87
+
88
+
89
+ def load_config() -> NormalizeConfig:
90
+ """Convenience loader (cached)."""
91
+ global _CONFIG
92
+ if _CONFIG is None:
93
+ _CONFIG = NormalizeConfig.from_env()
94
+ return _CONFIG
95
+
96
+
97
+ _CONFIG: Optional[NormalizeConfig] = None
@@ -0,0 +1,166 @@
1
+ """
2
+ winnex-ai-normalize — embedding providers (with failover).
3
+
4
+ Extracted from the Maestro's `EmbeddingService` (winnex-madhava-maestro):
5
+ connects to an OpenAI-compatible `/v1/embeddings` endpoint (e.g.
6
+ Qwen3-Embedding-0.6B), with a bounded LRU cache and L2 normalization.
7
+
8
+ NO fallback to fake vectors: if the configured providers are unavailable,
9
+ the error is explicit (a normalizer must not fabricate embeddings).
10
+
11
+ Provider failover: the providers are tried in `config.provider_order`
12
+ (lowest priority first); if one fails, the next is tried. If ALL fail,
13
+ a RuntimeError is raised with the collected errors.
14
+ """
15
+ import logging
16
+ import os
17
+ import threading
18
+ from typing import List, Optional
19
+
20
+ import numpy as np
21
+
22
+ logger = logging.getLogger("winnex-ai-normalize.embedding")
23
+
24
+
25
+ class EmbeddingProvider:
26
+ """A single embedding provider (OpenAI-compatible /v1/embeddings)."""
27
+
28
+ def __init__(self, config, http_client_factory=None):
29
+ self.name = config.name
30
+ self.model = config.model
31
+ self.base_url = config.base_url
32
+ self.timeout = config.timeout
33
+ self.api_key_env = config.api_key_env
34
+ self.dim = config.dim
35
+ self._http_factory = http_client_factory
36
+
37
+ def _post(self, path: str, payload: dict) -> dict:
38
+ import httpx
39
+ headers = {}
40
+ if self.api_key_env:
41
+ key = os.environ.get(self.api_key_env, "")
42
+ if key:
43
+ headers["Authorization"] = f"Bearer {key}"
44
+ with httpx.Client(timeout=self.timeout) as client:
45
+ resp = client.post(f"{self.base_url}{path}", json=payload, headers=headers)
46
+ resp.raise_for_status()
47
+ return resp.json()
48
+
49
+ def embed(self, texts: List[str]) -> np.ndarray:
50
+ """Embed a list of texts → (n, d) float32 L2-normalized."""
51
+ result = self._post("/embeddings", {"model": self.model, "input": texts})
52
+ data = sorted(result.get("data", []), key=lambda x: x.get("index", 0))
53
+ if not data:
54
+ raise RuntimeError(f"provider {self.name}: empty embeddings response")
55
+ vecs = np.array([d["embedding"] for d in data], dtype=np.float32)
56
+ norms = np.linalg.norm(vecs, axis=1, keepdims=True)
57
+ vecs = vecs / np.maximum(norms, 1e-12) # L2-normalize (cosine contract)
58
+ if not np.isfinite(vecs).all():
59
+ raise RuntimeError(f"provider {self.name}: embeddings contain NaN/inf")
60
+ self.dim = vecs.shape[1]
61
+ return np.ascontiguousarray(vecs, dtype=np.float32)
62
+
63
+ def check_available(self) -> dict:
64
+ try:
65
+ r = self._post("/embeddings", {"model": self.model, "input": ["health-check"]})
66
+ return {"available": bool(r.get("data")), "provider": self.name,
67
+ "model": self.model, "base_url": self.base_url}
68
+ except Exception as e:
69
+ return {"available": False, "provider": self.name, "error": str(e)}
70
+
71
+
72
+ class EmbeddingService:
73
+ """Embedding service with provider failover + bounded cache.
74
+
75
+ Tries the providers in `config.provider_order`; the first that succeeds
76
+ serves the batch. If ALL fail, raises RuntimeError with the collected
77
+ errors (NO silent fallback to fake vectors).
78
+ """
79
+
80
+ def __init__(self, config=None, provider_factory=EmbeddingProvider):
81
+ from .config import load_config
82
+ self.config = config or load_config()
83
+ self._providers = {
84
+ p.name: provider_factory(p) for p in self.config.providers
85
+ }
86
+ self._cache: dict[str, np.ndarray] = {}
87
+ self._lock = threading.Lock()
88
+
89
+ def _cache_put(self, text: str, vec: np.ndarray) -> None:
90
+ with self._lock:
91
+ if len(self._cache) >= max(self.config.cache_max, 1):
92
+ for old in list(self._cache)[: max(0, len(self._cache) - self.config.cache_max + 1)]:
93
+ self._cache.pop(old, None)
94
+ self._cache[text] = vec
95
+
96
+ def _embed_missing(self, texts: List[str]) -> np.ndarray:
97
+ """Embed via the first available provider (failover)."""
98
+ errors = []
99
+ for name in self.config.provider_order:
100
+ p = self._providers.get(name)
101
+ if not p or not p.__dict__.get("_available", True):
102
+ continue
103
+ try:
104
+ return p.embed(texts)
105
+ except Exception as e:
106
+ errors.append(f"{name}: {str(e)[:120]}")
107
+ logger.warning(f"provider {name} failed: {e}")
108
+ raise RuntimeError(
109
+ "EmbeddingService: ALL providers unavailable — refusing to "
110
+ f"fabricate embeddings. Errors: {'; '.join(errors) or 'none configured'}")
111
+
112
+ def embed_texts(self, texts: List[str], dim: Optional[int] = None) -> np.ndarray:
113
+ """Embed a list of texts → (n, d) float32, cache repeated texts.
114
+
115
+ Args:
116
+ texts: list of strings.
117
+ dim: expected dimension (validated).
118
+ Returns:
119
+ (n, d) float32 L2-normalized embeddings.
120
+ """
121
+ if not texts:
122
+ return np.zeros((0, dim or self.config.default_dim), dtype=np.float32)
123
+ with self._lock:
124
+ missing_idx = [i for i, t in enumerate(texts) if t not in self._cache]
125
+ if missing_idx:
126
+ missing_texts = [texts[i] for i in missing_idx]
127
+ result = self._embed_missing(missing_texts)
128
+ if dim and result.shape[1] != dim:
129
+ raise ValueError(
130
+ f"provider returned dim {result.shape[1]}, expected {dim}")
131
+ for i, v in zip(missing_idx, result):
132
+ self._cache_put(texts[i], v)
133
+ with self._lock:
134
+ vecs = np.stack([self._cache[t] for t in texts]).astype(np.float32)
135
+ return np.ascontiguousarray(vecs, dtype=np.float32)
136
+
137
+ def embed_one(self, text: str) -> np.ndarray:
138
+ """Embed a single text → (d,) float32."""
139
+ return self.embed_texts([text])[0]
140
+
141
+ def clear_cache(self) -> None:
142
+ with self._lock:
143
+ self._cache.clear()
144
+
145
+ def check_available(self) -> dict:
146
+ for name in self.config.provider_order:
147
+ p = self._providers.get(name)
148
+ if p:
149
+ r = p.check_available()
150
+ if r.get("available"):
151
+ return r
152
+ logger.warning(f"provider {name} unavailable: {r.get('error')}")
153
+ return {"available": False,
154
+ "error": "no provider available",
155
+ "tried": self.config.provider_order}
156
+
157
+
158
+ # Singleton for reuse
159
+ _service = None
160
+
161
+
162
+ def get_embedding_service() -> EmbeddingService:
163
+ global _service
164
+ if _service is None:
165
+ _service = EmbeddingService()
166
+ return _service
@@ -0,0 +1,188 @@
1
+ """
2
+ winnex-ai-normalize — embedding normalization (the plug that feeds Madhava).
3
+
4
+ The Madhava engine is AGNOSTIC: it consumes float32 vectors. This module
5
+ normalizes ANY input (text via a provider, or raw vectors) into a valid
6
+ float32 corpus ready for `winnex_madhava.build_engine`:
7
+
8
+ - validates dimension, dtype, NaN/inf (fail loudly — no silent fallback),
9
+ - L2-normalizes when the cosine contract requires it,
10
+ - quantizes float32 → uint8 for the engine's native corpus
11
+ (the exact scale logic from the Maestro's `quantize_corpus`).
12
+
13
+ License: Business Source License 1.1 (BSL 1.1)
14
+ """
15
+ import logging
16
+ from typing import Optional, Union
17
+
18
+ import numpy as np
19
+
20
+ logger = logging.getLogger("winnex-ai-normalize")
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Validation (fail loudly — no fake embeddings)
25
+ # ---------------------------------------------------------------------------
26
+ def validate_embeddings(
27
+ vectors: np.ndarray,
28
+ dim: Optional[int] = None,
29
+ require_unit_norm: bool = False,
30
+ ) -> np.ndarray:
31
+ """Validate a (n, d) embedding matrix. Raises on invalid input.
32
+
33
+ Args:
34
+ vectors: (n, d) float32 (or float64) embeddings.
35
+ dim: expected dimensionality (raises if mismatch).
36
+ require_unit_norm: if True, each row must be unit-norm (cosine).
37
+ Returns:
38
+ the array as float32, contiguous.
39
+ Raises:
40
+ ValueError: on wrong dim, NaN/inf, or empty.
41
+ """
42
+ arr = np.ascontiguousarray(vectors, dtype=np.float32)
43
+ if arr.ndim != 2:
44
+ raise ValueError(f"embeddings must be 2D (n, d), got shape {arr.shape}")
45
+ if arr.shape[0] == 0:
46
+ raise ValueError("embeddings must not be empty")
47
+ if dim is not None and arr.shape[1] != dim:
48
+ raise ValueError(
49
+ f"dimension mismatch: expected {dim}, got {arr.shape[1]}")
50
+ if not np.isfinite(arr).all():
51
+ raise ValueError("embeddings contain NaN or inf — refusing to proceed")
52
+ if require_unit_norm:
53
+ norms = np.linalg.norm(arr, axis=1)
54
+ bad = np.where((norms < 1e-6) | (np.abs(norms - 1.0) > 1e-2))[0]
55
+ if len(bad):
56
+ raise ValueError(
57
+ f"{len(bad)} rows are not unit-norm (cosine contract) — "
58
+ "call normalize_l2() first or fix the provider")
59
+ return arr
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # L2 normalization (cosine contract)
64
+ # ---------------------------------------------------------------------------
65
+ def normalize_l2(vectors: np.ndarray) -> np.ndarray:
66
+ """L2-normalize each row to unit norm (the cosine contract).
67
+
68
+ Raises on a zero-norm row (cannot be normalized meaningfully).
69
+ """
70
+ arr = validate_embeddings(vectors)
71
+ norms = np.linalg.norm(arr, axis=1, keepdims=True)
72
+ if (norms < 1e-12).any():
73
+ raise ValueError("a row has zero norm — cannot L2-normalize")
74
+ return arr / np.maximum(norms, 1e-12)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Quantization float32 → uint8 (the engine's native corpus)
79
+ # ---------------------------------------------------------------------------
80
+ def quantize_corpus(embeddings: np.ndarray) -> np.ndarray:
81
+ """Quantize float32 normalized embeddings to the engine's uint8 corpus.
82
+
83
+ The native C++ engine requires a uint8 corpus and interprets each byte as
84
+ an integer coordinate in [0, 255] (see `load_raw` in winnex-madhava: it
85
+ casts uint8 → float32 without re-scaling, then L2-normalizes). Passing raw
86
+ float32 normalized embeddings straight into `build_engine` silently
87
+ truncates them (`astype(np.uint8)` maps everything in [-1, 1] to 0) — a
88
+ zero corpus whose cosine scores are all 0.0.
89
+
90
+ This is the exact scale logic from the Maestro's `quantize_corpus`:
91
+ - values in [-1, 1] (embedding domain) → map to [0, 255] via (x+1)*127.5
92
+ - values in [0, 1] (post-activation) → scale to [0, 255] via x*255
93
+ - values already in [0, 255] → left as-is (round+clip)
94
+
95
+ Args:
96
+ embeddings: (n, d) float32 normalized embeddings (cosine).
97
+ Returns:
98
+ (n, d) uint8 corpus ready for `build_engine`.
99
+ """
100
+ arr = validate_embeddings(embeddings)
101
+ if arr.size and arr.min() < 0.0:
102
+ # Embedding-domain values (normalized vectors): [-1, 1] -> [0, 255].
103
+ u8 = np.round((arr + 1.0) * 127.5).clip(0, 255).astype(np.uint8)
104
+ else:
105
+ # [0, 1] normalized domain (Qwen post-activation): scale to [0, 255].
106
+ u8 = np.round(arr * 255.0).clip(0, 255).astype(np.uint8)
107
+ return np.ascontiguousarray(u8, dtype=np.uint8)
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # The normalizer facade (text/vectors → ready-for-madhava)
112
+ # ---------------------------------------------------------------------------
113
+ class EmbeddingNormalizer:
114
+ """Normalizes text or raw vectors into Madhava-ready float32/uint8."""
115
+
116
+ def __init__(self, config=None, embedding_service=None):
117
+ from .config import load_config
118
+ self.config = config or load_config()
119
+ self._embedding_service = embedding_service
120
+
121
+ @property
122
+ def embedding_service(self):
123
+ if self._embedding_service is None:
124
+ from .embedding import get_embedding_service
125
+ self._embedding_service = get_embedding_service()
126
+ return self._embedding_service
127
+
128
+ def vectorize_texts(self, texts, dim=None) -> np.ndarray:
129
+ """Embed texts via the configured provider, normalized to float32.
130
+
131
+ Raises if no provider is available (NO fallback to fake vectors).
132
+ """
133
+ dim = dim or self.config.default_dim
134
+ return self.embedding_service.embed_texts(texts, dim=dim)
135
+
136
+ def normalize_vectors(
137
+ self,
138
+ vectors: Union[np.ndarray, list],
139
+ dim: Optional[int] = None,
140
+ require_unit_norm: bool = False,
141
+ ) -> np.ndarray:
142
+ """Validate + normalize raw vectors to float32 (unit-norm optional)."""
143
+ arr = np.asarray(vectors, dtype=np.float32)
144
+ return validate_embeddings(arr, dim=dim, require_unit_norm=require_unit_norm)
145
+
146
+ def to_corpus(
147
+ self,
148
+ vectors: Union[np.ndarray, list],
149
+ dim: Optional[int] = None,
150
+ ) -> np.ndarray:
151
+ """Quantize float32 → uint8 (for BIGANN-style RAW BYTE corpora).
152
+
153
+ NOTE: this is NOT the cosine path. For float32 embeddings
154
+ (OpenAI/Qwen3) use `build_engine()` which goes through the engine's
155
+ float32 manifold (build_float32) and preserves cosine exactly.
156
+ """
157
+ arr = self.normalize_vectors(vectors, dim=dim)
158
+ return quantize_corpus(arr)
159
+
160
+ def build_engine(self, vectors, dim=None, k=10, **engine_kwargs):
161
+ """Build a Madhava engine over float32 embeddings (the CORRECT path).
162
+
163
+ Uses `winnex_madhava.build_engine` with the float32 corpus (the
164
+ engine's build_float32 manifold preserves cosine). The madhava
165
+ engine stays agnostic — it receives ready float32 vectors.
166
+
167
+ Args:
168
+ vectors: (n, d) float32 embeddings (or text via vectorize_texts).
169
+ dim: embedding dimension.
170
+ k: top-k.
171
+ **engine_kwargs: passed to winnex_madhava.build_engine.
172
+ Returns:
173
+ a winnex_madhava engine.
174
+ """
175
+ import winnex_madhava as wm
176
+ arr = self.normalize_vectors(vectors, dim=dim)
177
+ return wm.build_engine(
178
+ np.ascontiguousarray(arr, dtype=np.float32),
179
+ dim=dim or arr.shape[1],
180
+ metric="cosine",
181
+ k=k,
182
+ normalize_input=True,
183
+ **engine_kwargs,
184
+ )
185
+
186
+ def check_available(self) -> dict:
187
+ """Health check: is the embedding provider reachable?"""
188
+ return self.embedding_service.check_available()
@@ -0,0 +1,98 @@
1
+ """
2
+ winnex-ai-normalize — normalization tests (REAL data, no fallback).
3
+
4
+ Validates:
5
+ - validate_embeddings rejects NaN/wrong-dim (fail loudly).
6
+ - normalize_l2 produces unit-norm rows.
7
+ - quantize_corpus maps float32 → uint8 with the correct scale.
8
+ - EmbeddingNormalizer end-to-end with a REAL dataset (arXiv d=1536).
9
+
10
+ Run: python -m pytest winnex_ai_normalize/tests/ -v
11
+ """
12
+ import os
13
+
14
+ import numpy as np
15
+ import pytest
16
+
17
+ from winnex_ai_normalize.core.normalize import (
18
+ EmbeddingNormalizer,
19
+ validate_embeddings,
20
+ normalize_l2,
21
+ quantize_corpus,
22
+ )
23
+ from winnex_ai_normalize.core.config import NormalizeConfig
24
+
25
+
26
+ def test_validate_rejects_nan():
27
+ v = np.random.randn(10, 128).astype(np.float32)
28
+ v[3, 5] = np.nan
29
+ with pytest.raises(ValueError, match="NaN"):
30
+ validate_embeddings(v)
31
+
32
+
33
+ def test_validate_rejects_wrong_dim():
34
+ v = np.random.randn(10, 128).astype(np.float32)
35
+ with pytest.raises(ValueError, match="dimension"):
36
+ validate_embeddings(v, dim=64)
37
+
38
+
39
+ def test_validate_rejects_empty():
40
+ with pytest.raises(ValueError, match="empty"):
41
+ validate_embeddings(np.zeros((0, 128), dtype=np.float32))
42
+
43
+
44
+ def test_normalize_l2_unit_norm():
45
+ v = np.random.randn(50, 128).astype(np.float32)
46
+ n = normalize_l2(v)
47
+ norms = np.linalg.norm(n, axis=1)
48
+ assert np.allclose(norms, 1.0, atol=1e-5)
49
+
50
+
51
+ def test_quantize_corpus_scale():
52
+ """uint8 quantization maps to [0, 255] with the correct scale.
53
+
54
+ NOTE: uint8 quantization is for BIGANN-style RAW BYTE corpora. For
55
+ float32 embeddings (OpenAI/Qwen3) the correct path is build_float32
56
+ (the engine's float32 manifold preserves cosine exactly); quantizing
57
+ float32 → uint8 with a shift is lossy and NOT the cosine path.
58
+ """
59
+ v = np.random.randn(20, 128).astype(np.float32)
60
+ v /= np.linalg.norm(v, axis=1, keepdims=True)
61
+ u8 = quantize_corpus(v)
62
+ assert u8.dtype == np.uint8
63
+ assert u8.min() >= 0 and u8.max() <= 255
64
+
65
+
66
+ def test_build_float32_path_preserves_cosine():
67
+ """The float32 path (build_float32) preserves cosine — the correct
68
+ path for real embeddings (no uint8 quantization)."""
69
+ v = np.random.randn(20, 128).astype(np.float32)
70
+ v /= np.linalg.norm(v, axis=1, keepdims=True)
71
+ # After L2-normalization the cosine of the float32 IS the inner product.
72
+ c = float(v[0] @ v[1])
73
+ assert -1.0 <= c <= 1.0
74
+ # The normalizer validates + keeps float32 (no lossy uint8 for cosine).
75
+ from winnex_ai_normalize import validate_embeddings
76
+ out = validate_embeddings(v, dim=128)
77
+ assert np.allclose(out, v)
78
+
79
+
80
+ def test_normalizer_with_real_arxiv():
81
+ """End-to-end with the REAL arXiv OpenAI embeddings (d=1536)."""
82
+ path = "/home/wnnx_user/zenodo/arxiv_100k.npy"
83
+ if not os.path.exists(path):
84
+ pytest.skip("arxiv_100k.npy not present — skipping real-data test")
85
+ a = np.load(path, mmap_mode="r")
86
+ X = np.ascontiguousarray(a[:200]) # real embeddings d=1536
87
+ n = np.linalg.norm(X, axis=1, keepdims=True)
88
+ X = X / np.maximum(n, 1e-12)
89
+
90
+ cfg = NormalizeConfig(default_dim=1536)
91
+ norm = EmbeddingNormalizer(config=cfg)
92
+ # Vectors pass through unchanged (validated + float32).
93
+ out = norm.normalize_vectors(X, dim=1536)
94
+ assert out.shape == (200, 1536)
95
+ assert np.isfinite(out).all()
96
+ # Corpus quantization for the madhava engine.
97
+ u8 = norm.to_corpus(X, dim=1536)
98
+ assert u8.shape == (200, 1536) and u8.dtype == np.uint8
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: winnex-ai-normalize
3
+ Version: 1.0.0
4
+ Summary: Input normalization plug for the Madhava engine — embed text via any provider (OpenAI, Qwen3, nano, xfactor), validate/normalize to float32/uint8, with provider failover and NO fake-vector fallback. Consumed by winnex-madhava, winnex-tracer, Liferay and the Maestro.
5
+ Author-email: Winnex Brasil Soluções Empresariais LTDA-ME <pay@winnex.ai>
6
+ License: Business Source License 1.1
7
+
8
+ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
9
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
10
+
11
+ Parameters
12
+
13
+ Licensor: Winnex AI
14
+ Licensed Work: Tracer-GOV
15
+ Additional Use Grant: Free for Brazilian government agencies.
16
+ Any use by a public sector entity in Brazil is permitted.
17
+
18
+ Change Date: 2036-01-01
19
+ Change License: GNU General Public License v2.0 or later
20
+
21
+ For inquiries: pay@winnex.ai
22
+
23
+ Project-URL: Homepage, https://winnex.ai
24
+ Project-URL: Source, https://github.com/winnex-ai/winnex-ai-normalize
25
+ Keywords: embedding,normalize,madhava,vector,openai,qwen3,rag,semantic-search,normalizer,winnex,failover
26
+ Classifier: Development Status :: 4 - Beta
27
+ Classifier: Intended Audience :: Developers
28
+ Classifier: License :: Other/Proprietary License
29
+ Classifier: Programming Language :: Python :: 3
30
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
31
+ Requires-Python: >=3.10
32
+ Description-Content-Type: text/markdown
33
+ License-File: LICENSE
34
+ Requires-Dist: numpy>=1.24
35
+ Requires-Dist: httpx>=0.24
36
+ Provides-Extra: api
37
+ Requires-Dist: fastapi>=0.100; extra == "api"
38
+ Requires-Dist: uvicorn>=0.20; extra == "api"
39
+ Provides-Extra: madhava
40
+ Requires-Dist: winnex-madhava>=1.9.2; extra == "madhava"
41
+ Provides-Extra: all
42
+ Requires-Dist: fastapi>=0.100; extra == "all"
43
+ Requires-Dist: uvicorn>=0.20; extra == "all"
44
+ Requires-Dist: winnex-madhava>=1.9.2; extra == "all"
45
+ Dynamic: license-file
46
+
47
+ # winnex-ai-normalize
48
+
49
+ **The input-normalization plug for the Madhava engine.**
50
+
51
+ The Madhava engine is **agnostic** — it consumes float32 vectors. This
52
+ package normalizes **any** input (text via an embedding provider, or raw
53
+ vectors) into a valid float32/uint8 corpus ready for
54
+ `winnex_madhava.build_engine`, with **provider failover** and **no silent
55
+ fallback to fake vectors**.
56
+
57
+ ```
58
+ text / raw vectors
59
+
60
+
61
+ winnex-ai-normalize
62
+ ├── providers: OpenAI / Qwen3 / nano / xfactor / direct (failover order)
63
+ ├── validate: dim + NaN + norm (fail loudly)
64
+ ├── normalize: L2 unit-norm (cosine contract)
65
+ └── quantize: float32 → uint8 (the engine's native corpus)
66
+
67
+
68
+ winnex-madhava (agnostic) → top-K + Cauchy-Schwarz proof
69
+ ```
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install winnex-ai-normalize # core (numpy + httpx)
75
+ pip install winnex-ai-normalize[api] # + OpenAI-compatible /v1/embeddings
76
+ pip install winnex-ai-normalize[all] # + winnex-madhava
77
+ ```
78
+
79
+ ## Quick start
80
+
81
+ ```python
82
+ from winnex_ai_normalize import EmbeddingNormalizer
83
+
84
+ norm = EmbeddingNormalizer() # reads env / JSON config
85
+
86
+ # 1. Embed text via the configured provider (OpenAI / Qwen3 / ...)
87
+ vecs = norm.vectorize_texts(["hypertension treatment", "diabetes care"])
88
+ # vecs: (2, d) float32 L2-normalized
89
+
90
+ # 2. Or normalize raw vectors (validated, no NaN, unit-norm)
91
+ vecs = norm.normalize_vectors(my_embeddings, dim=1024)
92
+
93
+ # 3. Feed the Madhava engine (agnostic)
94
+ u8 = norm.to_corpus(vecs, dim=1024) # uint8 corpus for build_engine
95
+ import winnex_madhava as wm
96
+ engine = wm.build_engine(u8, dim=1024, metric="cosine", k=10)
97
+ ```
98
+
99
+ ## OpenAI-compatible API
100
+
101
+ ```bash
102
+ uvicorn winnex_ai_normalize.api.server:app --port 8102
103
+ curl -X POST http://localhost:8102/v1/embeddings \
104
+ -H "Content-Type: application/json" \
105
+ -d '{"model": "Qwen3-Embedding-0.6B", "input": ["text one", "text two"]}'
106
+ # → {"data": [{"embedding": [...], "index": 0}, ...], ...}
107
+ ```
108
+
109
+ ## Provider failover (no fake fallback)
110
+
111
+ Providers are tried in `provider_order` (JSON/env config). If **all**
112
+ fail, a `RuntimeError` is raised with the collected errors — a normalizer
113
+ must never fabricate embeddings.
114
+
115
+ ```json
116
+ {
117
+ "default_dim": 1024,
118
+ "default_provider": "qwen3",
119
+ "providers": [
120
+ {"name": "qwen3", "base_url": "http://winnex-embedding:8102",
121
+ "model": "/workspace/models/Qwen3-Embedding-0.6B", "priority": 1},
122
+ {"name": "openai", "base_url": "https://api.openai.com/v1",
123
+ "model": "text-embedding-3-small", "api_key_env": "OPENAI_API_KEY", "priority": 2}
124
+ ],
125
+ "provider_order": ["qwen3", "openai"]
126
+ }
127
+ ```
128
+
129
+ ## Consumed by
130
+
131
+ - **winnex-madhava** (direct — the engine stays agnostic)
132
+ - **winnex-tracer** (audit + commitment)
133
+ - **Liferay bridges** (tracer-gov-liferay, tracer-med-liferay)
134
+ - **Maestro** (winnex-ai-server / winnex-ai-engine) and any external tool
135
+
136
+ ## License
137
+
138
+ Business Source License 1.1 (BSL 1.1) | pay@winnex.ai |
139
+ Winnex Brasil Soluções Empresariais LTDA (CNPJ 58.364.637/0001-47)
@@ -0,0 +1,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ winnex_ai_normalize/__init__.py
5
+ winnex_ai_normalize.egg-info/PKG-INFO
6
+ winnex_ai_normalize.egg-info/SOURCES.txt
7
+ winnex_ai_normalize.egg-info/dependency_links.txt
8
+ winnex_ai_normalize.egg-info/requires.txt
9
+ winnex_ai_normalize.egg-info/top_level.txt
10
+ winnex_ai_normalize/api/__init__.py
11
+ winnex_ai_normalize/api/server.py
12
+ winnex_ai_normalize/core/__init__.py
13
+ winnex_ai_normalize/core/config.py
14
+ winnex_ai_normalize/core/embedding.py
15
+ winnex_ai_normalize/core/normalize.py
16
+ winnex_ai_normalize/tests/__init__.py
17
+ winnex_ai_normalize/tests/test_normalize.py
@@ -0,0 +1,14 @@
1
+ numpy>=1.24
2
+ httpx>=0.24
3
+
4
+ [all]
5
+ fastapi>=0.100
6
+ uvicorn>=0.20
7
+ winnex-madhava>=1.9.2
8
+
9
+ [api]
10
+ fastapi>=0.100
11
+ uvicorn>=0.20
12
+
13
+ [madhava]
14
+ winnex-madhava>=1.9.2
@@ -0,0 +1 @@
1
+ winnex_ai_normalize