pipe-broker 0.1.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.
- pipe_broker-0.1.0/PKG-INFO +45 -0
- pipe_broker-0.1.0/README.md +15 -0
- pipe_broker-0.1.0/pyproject.toml +58 -0
- pipe_broker-0.1.0/src/pipe/__init__.py +16 -0
- pipe_broker-0.1.0/src/pipe/api/__init__.py +10 -0
- pipe_broker-0.1.0/src/pipe/api/app.py +55 -0
- pipe_broker-0.1.0/src/pipe/api/dev.py +28 -0
- pipe_broker-0.1.0/src/pipe/api/routes.py +169 -0
- pipe_broker-0.1.0/src/pipe/api/server.py +28 -0
- pipe_broker-0.1.0/src/pipe/api/templates/status.html +29 -0
- pipe_broker-0.1.0/src/pipe/lib/__init__.py +44 -0
- pipe_broker-0.1.0/src/pipe/lib/core/__init__.py +30 -0
- pipe_broker-0.1.0/src/pipe/lib/core/base.py +111 -0
- pipe_broker-0.1.0/src/pipe/lib/core/chain.py +62 -0
- pipe_broker-0.1.0/src/pipe/lib/core/context.py +60 -0
- pipe_broker-0.1.0/src/pipe/lib/core/ops.py +62 -0
- pipe_broker-0.1.0/src/pipe/lib/core/pipe.py +154 -0
- pipe_broker-0.1.0/src/pipe/lib/detectors/__init__.py +11 -0
- pipe_broker-0.1.0/src/pipe/lib/detectors/generic.py +118 -0
- pipe_broker-0.1.0/src/pipe/lib/detectors/interface.py +25 -0
- pipe_broker-0.1.0/src/pipe/lib/experts/__init__.py +14 -0
- pipe_broker-0.1.0/src/pipe/lib/experts/generic.py +111 -0
- pipe_broker-0.1.0/src/pipe/lib/experts/interface.py +25 -0
- pipe_broker-0.1.0/src/pipe/lib/experts/legal/__init__.py +36 -0
- pipe_broker-0.1.0/src/pipe/lib/experts/legal/authority.py +26 -0
- pipe_broker-0.1.0/src/pipe/lib/experts/legal/catalog.py +98 -0
- pipe_broker-0.1.0/src/pipe/lib/experts/legal/operations.py +218 -0
- pipe_broker-0.1.0/src/pipe/lib/experts/workflow.py +42 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/__init__.py +34 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/flights.py +162 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/fx.py +116 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/geocode.py +120 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/interface.py +25 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/openrouter.py +118 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/satellite.py +177 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/stt.py +120 -0
- pipe_broker-0.1.0/src/pipe/lib/providers/weather.py +106 -0
- pipe_broker-0.1.0/src/pipe/lib/shared/__init__.py +56 -0
- pipe_broker-0.1.0/src/pipe/lib/shared/context.py +104 -0
- pipe_broker-0.1.0/src/pipe/lib/shared/interface.py +25 -0
- pipe_broker-0.1.0/src/pipe/lib/shared/jurisdiction.py +141 -0
- pipe_broker-0.1.0/src/pipe/lib/shared/locale.py +95 -0
- pipe_broker-0.1.0/src/pipe/lib/shared/registry.py +89 -0
- pipe_broker-0.1.0/src/pipe/lib/shared/scope.py +122 -0
- pipe_broker-0.1.0/src/pipe/lib/shared/types.py +136 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/__init__.py +62 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/finance/__init__.py +40 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/finance/currency.py +179 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/finance/math.py +141 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/finance/validate.py +53 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/flow.py +77 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/geo/__init__.py +22 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/geo/bbox.py +61 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/geo/distance.py +57 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/geo/geohash.py +69 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/search/__init__.py +26 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/__init__.py +30 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/_common.py +103 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/aeo.py +220 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/geo.py +208 -0
- pipe_broker-0.1.0/src/pipe/lib/utils/search/relevance/seo.py +258 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pipe-broker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Atomic, data-in/data-out operations for multilingual translation and domain-expertise knowledge export.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: pipe,operations,translation,nlp,fastapi,atomic,streaming
|
|
7
|
+
Author: amaagracodes
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Text Processing :: Linguistic
|
|
17
|
+
Requires-Dist: babel
|
|
18
|
+
Requires-Dist: dspy (>=3.3,<4)
|
|
19
|
+
Requires-Dist: fastapi
|
|
20
|
+
Requires-Dist: httpx
|
|
21
|
+
Requires-Dist: jinja2
|
|
22
|
+
Requires-Dist: python-multipart
|
|
23
|
+
Requires-Dist: uvicorn
|
|
24
|
+
Project-URL: Documentation, https://github.com/amaagracodes/pipe
|
|
25
|
+
Project-URL: Homepage, https://github.com/amaagracodes/pipe
|
|
26
|
+
Project-URL: Issues, https://github.com/amaagracodes/pipe/issues
|
|
27
|
+
Project-URL: Repository, https://github.com/amaagracodes/pipe
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Pipe
|
|
31
|
+
|
|
32
|
+
Atomic, data-in/data-out operations for multilingual translation and domain-expertise knowledge export.
|
|
33
|
+
|
|
34
|
+
The core library lives in `src/pipe` — installable as `pipe-broker`, imported as `pipe`. A thin, stateless API adapter in `src/api` runs as a Cloudflare Python Worker.
|
|
35
|
+
|
|
36
|
+
Build uses `poetry-core`; dependencies are managed with `uv`.
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import pipe
|
|
42
|
+
|
|
43
|
+
pipe.echo("hi")
|
|
44
|
+
```
|
|
45
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Pipe
|
|
2
|
+
|
|
3
|
+
Atomic, data-in/data-out operations for multilingual translation and domain-expertise knowledge export.
|
|
4
|
+
|
|
5
|
+
The core library lives in `src/pipe` — installable as `pipe-broker`, imported as `pipe`. A thin, stateless API adapter in `src/api` runs as a Cloudflare Python Worker.
|
|
6
|
+
|
|
7
|
+
Build uses `poetry-core`; dependencies are managed with `uv`.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import pipe
|
|
13
|
+
|
|
14
|
+
pipe.echo("hi")
|
|
15
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pipe-broker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Atomic, data-in/data-out operations for multilingual translation and domain-expertise knowledge export."
|
|
5
|
+
authors = [{ name = "amaagracodes" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
license = { text = "MIT" }
|
|
9
|
+
keywords = ["pipe", "operations", "translation", "nlp", "fastapi", "atomic", "streaming"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
"Topic :: Text Processing :: Linguistic",
|
|
19
|
+
]
|
|
20
|
+
# All runtime deps are installed together — one install path, no optional
|
|
21
|
+
# extras to forget. The service (Cloud Run, real CPython) always needs the
|
|
22
|
+
# serving layer (FastAPI/Jinja2/uvicorn) and the LLM backend (DSPy, which
|
|
23
|
+
# brings its own LiteLLM client that speaks OpenRouter via openrouter/<model>).
|
|
24
|
+
dependencies = [
|
|
25
|
+
"fastapi",
|
|
26
|
+
"jinja2",
|
|
27
|
+
"uvicorn",
|
|
28
|
+
"dspy>=3.3,<4",
|
|
29
|
+
"httpx",
|
|
30
|
+
"python-multipart",
|
|
31
|
+
"babel",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/amaagracodes/pipe"
|
|
36
|
+
Repository = "https://github.com/amaagracodes/pipe"
|
|
37
|
+
Documentation = "https://github.com/amaagracodes/pipe"
|
|
38
|
+
Issues = "https://github.com/amaagracodes/pipe/issues"
|
|
39
|
+
|
|
40
|
+
# Dev-only tooling (uv reads this): tests + a local ASGI runner.
|
|
41
|
+
# The website is built with MkDocs Material (the same static-site generator
|
|
42
|
+
# DSPy's docs use). It is a docs/build-time dependency only — the runtime
|
|
43
|
+
# service just serves the already-built static output from public/.
|
|
44
|
+
[dependency-groups]
|
|
45
|
+
dev = ["pytest>=8", "uvicorn>=0.30"]
|
|
46
|
+
docs = ["mkdocs-material>=9.5"]
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["poetry-core>=2.0"]
|
|
50
|
+
build-backend = "poetry.core.masonry.api"
|
|
51
|
+
|
|
52
|
+
[tool.poetry]
|
|
53
|
+
packages = [{ include = "pipe", from = "src" }]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
# Do not write .pytest_cache or __pycache__ during test runs.
|
|
58
|
+
addopts = "-p no:cacheprovider"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""pipe — atomic, data-in/data-out operations for multilingual translation
|
|
2
|
+
and domain-expertise knowledge export.
|
|
3
|
+
|
|
4
|
+
One top-level package with two parts:
|
|
5
|
+
* pipe.lib — the pure, dependency-free core ops (imported here for a flat
|
|
6
|
+
public API: ``import pipe; pipe.echo("hi")``).
|
|
7
|
+
* pipe.api — the optional serving adapter (FastAPI / Cloudflare Worker).
|
|
8
|
+
NOT imported here, so the core stays dependency-free; import it explicitly
|
|
9
|
+
(``from pipe.api.app import create_app``) with the ``[api]`` extra installed.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pipe.lib import Chain, Echo, EchoStream, Pipe, echo, echo_stream
|
|
13
|
+
|
|
14
|
+
__all__ = ["Pipe", "Chain", "Echo", "EchoStream", "echo", "echo_stream"]
|
|
15
|
+
|
|
16
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""pipe.api — a thin, stateless serving layer over the pipe.lib core ops.
|
|
2
|
+
|
|
3
|
+
An *adapter*: it wires the atomic operations in ``pipe.lib`` to an HTTP surface
|
|
4
|
+
(FastAPI) suitable for running as a Cloudflare Python Worker. It owns no
|
|
5
|
+
business logic. Requires the ``[api]`` extra (fastapi, jinja2).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pipe.api.app import create_app
|
|
9
|
+
|
|
10
|
+
__all__ = ["create_app"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""FastAPI application factory.
|
|
2
|
+
|
|
3
|
+
Builds the app and mounts routes. Two frontend paths coexist by design:
|
|
4
|
+
|
|
5
|
+
* Static assets (``public/``) — the landing page and any purely-static files.
|
|
6
|
+
On Cloudflare these are served via the Workers ASSETS binding; locally via
|
|
7
|
+
Starlette's StaticFiles. Best for content with no per-request data.
|
|
8
|
+
|
|
9
|
+
* Jinja2 templates (``pipe/api/templates/``) — for API-driven, server-rendered
|
|
10
|
+
pages that DO need per-request/dynamic data. Kept for API-based sites.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from fastapi import FastAPI
|
|
17
|
+
from fastapi.templating import Jinja2Templates
|
|
18
|
+
|
|
19
|
+
import pipe
|
|
20
|
+
|
|
21
|
+
_BASE_DIR = Path(__file__).resolve().parent
|
|
22
|
+
# The static frontend directory. Priced from an env var first (set in the
|
|
23
|
+
# container image to /app/public), falling back to the repo layout for local
|
|
24
|
+
# dev (src/pipe/api/app.py -> repo root is 4 levels up). When the package is
|
|
25
|
+
# pip-installed the repo-relative guess won't exist, which is why the container
|
|
26
|
+
# sets PIPE_PUBLIC_DIR explicitly.
|
|
27
|
+
PUBLIC_DIR = Path(
|
|
28
|
+
os.environ.get("PIPE_PUBLIC_DIR", str(Path(__file__).resolve().parents[3] / "public"))
|
|
29
|
+
)
|
|
30
|
+
# Server-side templates for dynamic, API-driven pages.
|
|
31
|
+
TEMPLATES = Jinja2Templates(directory=str(_BASE_DIR / "templates"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_app() -> FastAPI:
|
|
35
|
+
# When served behind a reverse proxy under a path prefix (e.g.
|
|
36
|
+
# /proxy/8787), set PIPE_ROOT_PATH to that prefix so FastAPI generates the
|
|
37
|
+
# correct URLs for /docs, /openapi.json, etc. Empty (default) = served at
|
|
38
|
+
# the domain root, as on Cloudflare Workers.
|
|
39
|
+
root_path = os.environ.get("PIPE_ROOT_PATH", "")
|
|
40
|
+
|
|
41
|
+
app = FastAPI(
|
|
42
|
+
title="pipe-broker",
|
|
43
|
+
version=pipe.__version__,
|
|
44
|
+
description=(
|
|
45
|
+
"Atomic, data-in/data-out operations for multilingual translation "
|
|
46
|
+
"and domain-expertise knowledge export."
|
|
47
|
+
),
|
|
48
|
+
root_path=root_path,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Routes are registered separately to keep concerns split.
|
|
52
|
+
from pipe.api.routes import register_routes
|
|
53
|
+
|
|
54
|
+
register_routes(app)
|
|
55
|
+
return app
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Local development server.
|
|
2
|
+
|
|
3
|
+
Runs the exact same FastAPI app as the Cloudflare Worker, but via uvicorn so
|
|
4
|
+
it can be tested locally without the Workers/Pyodide runtime.
|
|
5
|
+
|
|
6
|
+
uv run --extra api --with uvicorn python -m pipe.api.dev
|
|
7
|
+
|
|
8
|
+
Binds 0.0.0.0 by default so it is reachable on localhost, LAN, and tailnet.
|
|
9
|
+
Override with PIPE_HOST / PIPE_PORT.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import uvicorn
|
|
15
|
+
|
|
16
|
+
from pipe.api.app import create_app
|
|
17
|
+
|
|
18
|
+
app = create_app()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> None:
|
|
22
|
+
host = os.environ.get("PIPE_HOST", "0.0.0.0")
|
|
23
|
+
port = int(os.environ.get("PIPE_PORT", "8787"))
|
|
24
|
+
uvicorn.run(app, host=host, port=port, log_level="info")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""HTTP route handlers.
|
|
2
|
+
|
|
3
|
+
Each handler is thin and async: parse input, call a ``pipe.lib`` atomic op,
|
|
4
|
+
return output. Three response styles coexist:
|
|
5
|
+
|
|
6
|
+
* JSON ops — /echo (buffered)
|
|
7
|
+
* streaming ops — /echo/stream (incremental data stream)
|
|
8
|
+
* server-rendered — /status (Jinja2 template, for API-driven dynamic pages)
|
|
9
|
+
|
|
10
|
+
The static frontend (public/) is mounted last at "/" so API routes and /docs
|
|
11
|
+
take priority; index.html is served at "/".
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from fastapi import FastAPI, Request, UploadFile
|
|
15
|
+
from fastapi.responses import HTMLResponse, StreamingResponse
|
|
16
|
+
from fastapi.staticfiles import StaticFiles
|
|
17
|
+
|
|
18
|
+
import pipe
|
|
19
|
+
from pipe.api.app import PUBLIC_DIR, TEMPLATES
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def register_routes(app: FastAPI) -> None:
|
|
23
|
+
@app.get("/echo", tags=["ops"])
|
|
24
|
+
async def echo(data: str = "") -> dict:
|
|
25
|
+
"""Echo atomic op: returns the input unchanged (data in, data out)."""
|
|
26
|
+
return {"data": pipe.echo(data)}
|
|
27
|
+
|
|
28
|
+
@app.get("/echo/stream", tags=["ops"])
|
|
29
|
+
async def echo_stream(data: str = "", chunk_size: int = 16) -> StreamingResponse:
|
|
30
|
+
"""Streaming echo: sends the input back incrementally as a data stream."""
|
|
31
|
+
return StreamingResponse(
|
|
32
|
+
pipe.echo_stream(data, chunk_size=chunk_size),
|
|
33
|
+
media_type="text/plain; charset=utf-8",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# --- LLM (OpenRouter) -------------------------------------------------
|
|
37
|
+
|
|
38
|
+
@app.post("/llm", tags=["llm"])
|
|
39
|
+
async def llm(prompt: str, model: str = "openai/gpt-4o-mini", max_tokens: int = 256) -> dict:
|
|
40
|
+
"""Test route: send a prompt to OpenRouter and return the completion.
|
|
41
|
+
|
|
42
|
+
Reads the key from $OPENROUTER_API_KEY (injected in the deployment).
|
|
43
|
+
Returns a 4xx-shaped JSON error rather than a 500 when the LLM stack or
|
|
44
|
+
key is unavailable, so it's safe to probe.
|
|
45
|
+
"""
|
|
46
|
+
from fastapi import HTTPException
|
|
47
|
+
|
|
48
|
+
from pipe.lib.providers import OpenRouterProvider
|
|
49
|
+
|
|
50
|
+
provider = OpenRouterProvider(model=model, max_tokens=max_tokens)
|
|
51
|
+
try:
|
|
52
|
+
text = await provider.acall(prompt)
|
|
53
|
+
except ModuleNotFoundError as exc:
|
|
54
|
+
raise HTTPException(status_code=501, detail=str(exc)) from exc
|
|
55
|
+
except RuntimeError as exc: # missing key
|
|
56
|
+
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
57
|
+
except Exception as exc: # upstream/LLM error — don't leak internals
|
|
58
|
+
raise HTTPException(status_code=502, detail=f"LLM call failed: {type(exc).__name__}") from exc
|
|
59
|
+
return {"model": provider.model, "prompt": prompt, "completion": text}
|
|
60
|
+
|
|
61
|
+
# --- legal atomic ops -------------------------------------------------
|
|
62
|
+
|
|
63
|
+
@app.get("/legal/ops", tags=["legal"])
|
|
64
|
+
async def legal_ops() -> dict:
|
|
65
|
+
"""List the available atomic legal operations and rule categories."""
|
|
66
|
+
from pipe.lib.experts import legal
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"operations": [
|
|
70
|
+
"detect_pii", "spot_clauses", "identify_statutes",
|
|
71
|
+
"extract_citations", "screen_privilege",
|
|
72
|
+
],
|
|
73
|
+
"categories": sorted(legal.categories()),
|
|
74
|
+
"rule_count": len(legal.RULES),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@app.post("/legal/detect", tags=["legal"])
|
|
78
|
+
async def legal_detect(text: str, jurisdiction: str = "us", op: str = "detect_pii") -> dict:
|
|
79
|
+
"""Run one atomic legal op over text for a given jurisdiction.
|
|
80
|
+
|
|
81
|
+
``op`` is one of the names from ``GET /legal/ops``; ``jurisdiction`` is a
|
|
82
|
+
hierarchical code like ``us``, ``us/ny``, ``in/mh`` (optionally with
|
|
83
|
+
facets appended later). Returns the detections found.
|
|
84
|
+
"""
|
|
85
|
+
from fastapi import HTTPException
|
|
86
|
+
|
|
87
|
+
from pipe.lib.experts.legal import (
|
|
88
|
+
DetectPII, ExtractCitations, IdentifyStatutes, ScreenPrivilege, SpotClauses,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
ops = {
|
|
92
|
+
"detect_pii": DetectPII,
|
|
93
|
+
"spot_clauses": SpotClauses,
|
|
94
|
+
"identify_statutes": IdentifyStatutes,
|
|
95
|
+
"extract_citations": ExtractCitations,
|
|
96
|
+
"screen_privilege": ScreenPrivilege,
|
|
97
|
+
}
|
|
98
|
+
op_cls = ops.get(op)
|
|
99
|
+
if op_cls is None:
|
|
100
|
+
raise HTTPException(status_code=400, detail=f"unknown op {op!r}; choose from {sorted(ops)}")
|
|
101
|
+
result = op_cls(jurisdiction)(text)
|
|
102
|
+
return {
|
|
103
|
+
"op": op,
|
|
104
|
+
"jurisdiction": jurisdiction,
|
|
105
|
+
"detections": [
|
|
106
|
+
{
|
|
107
|
+
"kind": d.kind,
|
|
108
|
+
"label": d.label,
|
|
109
|
+
"severity": d.severity.name,
|
|
110
|
+
"span": [d.span.start, d.span.end] if d.span else None,
|
|
111
|
+
}
|
|
112
|
+
for d in result.detections
|
|
113
|
+
],
|
|
114
|
+
"meta": result.document.meta,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# --- speech-to-text (OpenRouter) --------------------------------------
|
|
118
|
+
|
|
119
|
+
@app.post("/stt", tags=["llm"])
|
|
120
|
+
async def stt(
|
|
121
|
+
file: UploadFile,
|
|
122
|
+
model: str = "openai/whisper-large-v3",
|
|
123
|
+
language: str | None = None,
|
|
124
|
+
) -> dict:
|
|
125
|
+
"""Transcribe an uploaded audio file to text (audio bytes in, text out).
|
|
126
|
+
|
|
127
|
+
Accepts a multipart file upload; the audio format is inferred from the
|
|
128
|
+
filename extension (falls back to ``wav``). Reads the key from
|
|
129
|
+
$OPENROUTER_API_KEY. Errors map like /llm (503 missing key, 502 upstream).
|
|
130
|
+
"""
|
|
131
|
+
from fastapi import HTTPException
|
|
132
|
+
|
|
133
|
+
from pipe.lib.providers import STT
|
|
134
|
+
|
|
135
|
+
audio = await file.read()
|
|
136
|
+
# Derive the container format from the upload's extension.
|
|
137
|
+
fmt = "wav"
|
|
138
|
+
if file.filename and "." in file.filename:
|
|
139
|
+
fmt = file.filename.rsplit(".", 1)[-1].lower()
|
|
140
|
+
|
|
141
|
+
provider = STT(model=model, language=language)
|
|
142
|
+
try:
|
|
143
|
+
text = await provider.acall(audio, format=fmt)
|
|
144
|
+
except RuntimeError as exc: # missing key
|
|
145
|
+
raise HTTPException(status_code=503, detail=str(exc)) from exc
|
|
146
|
+
except Exception as exc: # upstream/transcription error — don't leak internals
|
|
147
|
+
raise HTTPException(status_code=502, detail=f"STT call failed: {type(exc).__name__}") from exc
|
|
148
|
+
return {"model": provider.model, "format": fmt, "text": text}
|
|
149
|
+
|
|
150
|
+
@app.get("/status", response_class=HTMLResponse, include_in_schema=False)
|
|
151
|
+
async def status(request: Request, data: str = "pong"):
|
|
152
|
+
"""Server-rendered (Jinja2) page — kept for dynamic, API-driven sites."""
|
|
153
|
+
return TEMPLATES.TemplateResponse(
|
|
154
|
+
request,
|
|
155
|
+
"status.html",
|
|
156
|
+
{
|
|
157
|
+
"version": pipe.__version__,
|
|
158
|
+
"echoed": pipe.echo(data),
|
|
159
|
+
"ops": ["echo", "echo_stream"],
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Static frontend, mounted LAST so the API routes above (and /docs,
|
|
164
|
+
# /openapi.json) match first. html=True serves index.html at "/". The
|
|
165
|
+
# public/ directory ships in the container image (and exists in local dev).
|
|
166
|
+
# The is_dir guard just keeps the app importable in environments where the
|
|
167
|
+
# directory isn't present (e.g. running the ops library without the site).
|
|
168
|
+
if PUBLIC_DIR.is_dir():
|
|
169
|
+
app.mount("/", StaticFiles(directory=str(PUBLIC_DIR), html=True), name="site")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Production ASGI server entrypoint (container / Cloud Run).
|
|
2
|
+
|
|
3
|
+
Cloud Run injects the port to listen on via the ``PORT`` env var (default 8080)
|
|
4
|
+
and expects the process to bind 0.0.0.0. This module exposes the ASGI ``app``
|
|
5
|
+
for ``uvicorn pipe.api.server:app`` and a ``main()`` for direct execution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
import uvicorn
|
|
11
|
+
|
|
12
|
+
from pipe.api.app import create_app
|
|
13
|
+
|
|
14
|
+
app = create_app()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
port = int(os.environ.get("PORT", "8080"))
|
|
19
|
+
uvicorn.run(
|
|
20
|
+
"pipe.api.server:app",
|
|
21
|
+
host="0.0.0.0",
|
|
22
|
+
port=port,
|
|
23
|
+
log_level=os.environ.get("LOG_LEVEL", "info"),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>pipe-broker · status</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div class="glow" aria-hidden="true"></div>
|
|
11
|
+
<main>
|
|
12
|
+
<section class="hero">
|
|
13
|
+
<span class="pill">server-rendered</span>
|
|
14
|
+
<h1 class="grad">pipe-broker status</h1>
|
|
15
|
+
<p class="sub">
|
|
16
|
+
This page is rendered server-side from a Jinja2 template — the path we
|
|
17
|
+
keep for dynamic, API-driven pages.
|
|
18
|
+
</p>
|
|
19
|
+
<div class="panel" style="max-width:32rem;margin:0 auto;text-align:left">
|
|
20
|
+
<p><strong>Version:</strong> {{ version }}</p>
|
|
21
|
+
<p><strong>Echoed value:</strong> {{ echoed }}</p>
|
|
22
|
+
<p><strong>Ops available:</strong>
|
|
23
|
+
{% for op in ops %}<code>{{ op }}</code>{% if not loop.last %}, {% endif %}{% endfor %}
|
|
24
|
+
</p>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
</main>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""pipe.lib — the pure, stateless core: atomic data-in/data-out operations.
|
|
2
|
+
|
|
3
|
+
Zero runtime dependencies. No I/O, no framework, no global state. Safe to run
|
|
4
|
+
anywhere Python does, including Cloudflare's Pyodide runtime.
|
|
5
|
+
|
|
6
|
+
The interface primitives live in the modular :mod:`pipe.lib.core` subpackage
|
|
7
|
+
and are re-exported here for a flat import surface. Two ways to use an op:
|
|
8
|
+
|
|
9
|
+
* the interface — subclass :class:`Pipe`, implement ``forward``/``stream``,
|
|
10
|
+
invoke by calling the instance, and chain with ``>>``. This is the
|
|
11
|
+
DSPy-style module pattern: a small, composable, uniformly-called atomic
|
|
12
|
+
layer.
|
|
13
|
+
* the flat helpers — ``echo`` / ``echo_stream`` remain as functions for
|
|
14
|
+
callers that just want data in, data out.
|
|
15
|
+
|
|
16
|
+
Higher layers build on this: the generic modules :mod:`~pipe.lib.shared`,
|
|
17
|
+
:mod:`~pipe.lib.providers`, :mod:`~pipe.lib.experts`, :mod:`~pipe.lib.detectors`,
|
|
18
|
+
and the domain operations in :mod:`~pipe.lib.experts.legal`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from pipe.lib.core import (
|
|
22
|
+
BasePipe,
|
|
23
|
+
Chain,
|
|
24
|
+
Echo,
|
|
25
|
+
EchoStream,
|
|
26
|
+
Pipe,
|
|
27
|
+
PipeMeta,
|
|
28
|
+
echo,
|
|
29
|
+
echo_stream,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
# interface layer
|
|
34
|
+
"BasePipe",
|
|
35
|
+
"Pipe",
|
|
36
|
+
"PipeMeta",
|
|
37
|
+
"Chain",
|
|
38
|
+
# concrete ops
|
|
39
|
+
"Echo",
|
|
40
|
+
"EchoStream",
|
|
41
|
+
# flat function API
|
|
42
|
+
"echo",
|
|
43
|
+
"echo_stream",
|
|
44
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""pipe.lib.core — the interface primitives for atomic ops.
|
|
2
|
+
|
|
3
|
+
A modular, DSPy-shaped module system split across small files:
|
|
4
|
+
|
|
5
|
+
* :mod:`~pipe.lib.core.base` — ``BasePipe`` (structural) + ``PipeMeta``.
|
|
6
|
+
* :mod:`~pipe.lib.core.pipe` — ``Pipe``, the atomic callable interface.
|
|
7
|
+
* :mod:`~pipe.lib.core.chain` — ``Chain``, the composite that sequences ops.
|
|
8
|
+
* :mod:`~pipe.lib.core.ops` — concrete ops (``Echo``, ``EchoStream``) and
|
|
9
|
+
the flat ``echo`` / ``echo_stream`` function API.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from pipe.lib.core.base import BasePipe, PipeMeta
|
|
13
|
+
from pipe.lib.core.chain import Chain
|
|
14
|
+
from pipe.lib.core.context import current_context, require_context, use_context
|
|
15
|
+
from pipe.lib.core.ops import Echo, EchoStream, echo, echo_stream
|
|
16
|
+
from pipe.lib.core.pipe import Pipe
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"BasePipe",
|
|
20
|
+
"PipeMeta",
|
|
21
|
+
"Pipe",
|
|
22
|
+
"Chain",
|
|
23
|
+
"Echo",
|
|
24
|
+
"EchoStream",
|
|
25
|
+
"echo",
|
|
26
|
+
"echo_stream",
|
|
27
|
+
"current_context",
|
|
28
|
+
"use_context",
|
|
29
|
+
"require_context",
|
|
30
|
+
]
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""pipe.lib.core.base — the structural layer + metaclass.
|
|
2
|
+
|
|
3
|
+
DSPy splits its abstraction in two:
|
|
4
|
+
|
|
5
|
+
* ``BaseModule`` — the *structural* layer. It knows nothing about calling;
|
|
6
|
+
it only walks an instance's attributes to discover nested sub-modules,
|
|
7
|
+
enabling introspection, composition and copying.
|
|
8
|
+
* ``Module`` — the *behavioural* layer (see :mod:`pipe.lib.core.pipe`).
|
|
9
|
+
|
|
10
|
+
``BasePipe`` here is that structural layer for pipe, and ``PipeMeta`` is the
|
|
11
|
+
metaclass that guarantees base attributes exist on every ``Pipe`` — analogous
|
|
12
|
+
to DSPy's ``ProgramMeta``.
|
|
13
|
+
|
|
14
|
+
No third-party imports: safe under Cloudflare's Pyodide runtime.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import copy
|
|
20
|
+
from typing import TYPE_CHECKING, Any
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pipe.lib.core.pipe import Pipe
|
|
24
|
+
|
|
25
|
+
__all__ = ["BasePipe", "PipeMeta"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BasePipe:
|
|
29
|
+
"""Structural layer: attribute-walking, discovery, and copying.
|
|
30
|
+
|
|
31
|
+
Analogous to ``dspy.primitives.BaseModule``. Holds no calling logic — it
|
|
32
|
+
only understands how ``Pipe``s nest inside one another so that a composite
|
|
33
|
+
``Pipe`` can be introspected and copied as a unit.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def named_pipes(self) -> list[tuple[str, "Pipe"]]:
|
|
37
|
+
"""Return ``(name, pipe)`` for every ``Pipe`` reachable from ``self``.
|
|
38
|
+
|
|
39
|
+
Walks ``__dict__`` one level deep, descending into nested ``Pipe``s and
|
|
40
|
+
into lists/tuples/dicts of ``Pipe``s. Names use dotted / indexed paths
|
|
41
|
+
(``"encoder"``, ``"stages[0]"``, ``"routes['en']"``) so each sub-pipe
|
|
42
|
+
has a stable address. Mirrors ``BaseModule.named_parameters``.
|
|
43
|
+
"""
|
|
44
|
+
from pipe.lib.core.pipe import Pipe
|
|
45
|
+
|
|
46
|
+
visited: set[int] = set()
|
|
47
|
+
found: list[tuple[str, "Pipe"]] = []
|
|
48
|
+
|
|
49
|
+
def add(name: str, value: Any) -> None:
|
|
50
|
+
if isinstance(value, Pipe):
|
|
51
|
+
if id(value) in visited:
|
|
52
|
+
return
|
|
53
|
+
visited.add(id(value))
|
|
54
|
+
found.append((name, value))
|
|
55
|
+
# Descend so composites of composites are fully discovered.
|
|
56
|
+
for sub_name, sub in value.named_pipes():
|
|
57
|
+
child = f"{name}.{sub_name}"
|
|
58
|
+
if id(sub) not in visited:
|
|
59
|
+
visited.add(id(sub))
|
|
60
|
+
found.append((child, sub))
|
|
61
|
+
elif isinstance(value, (list, tuple)):
|
|
62
|
+
for i, item in enumerate(value):
|
|
63
|
+
add(f"{name}[{i}]", item)
|
|
64
|
+
elif isinstance(value, dict):
|
|
65
|
+
for key, item in value.items():
|
|
66
|
+
add(f"{name}[{key!r}]", item)
|
|
67
|
+
|
|
68
|
+
for name, value in self.__dict__.items():
|
|
69
|
+
add(name, value)
|
|
70
|
+
return found
|
|
71
|
+
|
|
72
|
+
def pipes(self) -> list["Pipe"]:
|
|
73
|
+
"""All nested ``Pipe`` instances (names dropped)."""
|
|
74
|
+
return [p for _, p in self.named_pipes()]
|
|
75
|
+
|
|
76
|
+
def deepcopy(self) -> "BasePipe":
|
|
77
|
+
"""Deep copy, falling back to a shallow attribute copy if needed.
|
|
78
|
+
|
|
79
|
+
Mirrors ``BaseModule.deepcopy``: a plain ``copy.deepcopy`` when the
|
|
80
|
+
instance allows it, otherwise a best-effort attribute-by-attribute copy.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
return copy.deepcopy(self)
|
|
84
|
+
except Exception:
|
|
85
|
+
new = self.__class__.__new__(self.__class__)
|
|
86
|
+
for attr, value in self.__dict__.items():
|
|
87
|
+
try:
|
|
88
|
+
setattr(new, attr, copy.deepcopy(value))
|
|
89
|
+
except Exception:
|
|
90
|
+
setattr(new, attr, copy.copy(value))
|
|
91
|
+
return new
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PipeMeta(type):
|
|
95
|
+
"""Metaclass guaranteeing base attributes exist on every ``Pipe``.
|
|
96
|
+
|
|
97
|
+
Analogous to DSPy's ``ProgramMeta``: it runs ``Pipe._base_init`` before the
|
|
98
|
+
subclass ``__init__`` so a subclass that forgets ``super().__init__()``
|
|
99
|
+
still has the attributes the framework relies on (here: ``name``).
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __call__(cls, *args: Any, **kwargs: Any) -> Any:
|
|
103
|
+
from pipe.lib.core.pipe import Pipe
|
|
104
|
+
|
|
105
|
+
obj = cls.__new__(cls, *args, **kwargs)
|
|
106
|
+
if isinstance(obj, cls):
|
|
107
|
+
Pipe._base_init(obj)
|
|
108
|
+
cls.__init__(obj, *args, **kwargs)
|
|
109
|
+
if not hasattr(obj, "name"):
|
|
110
|
+
obj.name = cls.__name__
|
|
111
|
+
return obj
|