This release is a pre-release and may not be stable for production use.
Loopiter for Python
Native, async Python SDK for reviewed, evidence-driven AI improvements. No Node.js subprocess, hosted service, model dependency, telemetry, or core runtime dependencies. Python 3.11+, MIT. Python alpha version: 0.2.0a1 (PEP 440).
Install the Python alpha
python -m pip install 'loopiter==0.2.0a1'
# Optional PostgreSQL adapter:
python -m pip install 'loopiter[postgres]==0.2.0a1'
Use an explicit version for this prerelease. The Node.js SDK is distributed separately on npm; installing either SDK does not install the other.
Run the offline example from source
git clone https://github.com/SanaanKhalid/loopiter.git
cd loopiter
python3.11 -m venv .venv
source .venv/bin/activate
python -m pip install ./python
python python/examples/reviewed_loop.py
python python/examples/reviewed_loop.py --reject
python python/examples/reviewed_loop.py --interrupt
Use an installed Python 3.11 or newer. The example runs offline, prints measured fixture metrics, and demonstrates explicit approval, a real change to its in-process classifier, rollback and interrupted-apply recovery. Simulated fixtures, not LLM performance evidence. Its in-memory deployment registry is not production storage.
Capture feedback
import asyncio
from loopiter import FeedbackLoop, InMemoryStore
async def main():
loop = FeedbackLoop(store=InMemoryStore(), namespace="support/dev")
execution = await loop.record_execution(
id="request-123",
kind="prediction",
episode_id="ticket-123",
input={"text": "Please explain this charge."},
output={"label": "other"},
artifacts={"model": "your-model-version", "prompt": "intent-v1"},
metadata={"intent": "billing"},
)
await loop.record_signal(
id="review-123",
execution_id=execution["id"],
kind="correction",
name="verified_correct",
value=False,
correction={"label": "billing"},
source="authorized-reviewer",
confidence=1,
)
findings = await loop.analyze(
dimensions=["metadata.intent"],
minimum_support=1,
minimum_scored_count=1,
)
print(findings)
await loop.close()
if __name__ == "__main__":
asyncio.run(main())
All records are detached plain dictionaries using snake_case; access IDs with
record["id"]. Use await in an existing async application instead of nesting
asyncio.run. Inputs must be plain JSON; timestamps are ISO UTC strings ending in
Z. Unknown fields, NaN/infinity, non-string keys, cycles, NUL/lone-surrogate strings and missing safety fields
fail closed. Use explicit IDs to retry identical sanitized input idempotently;
conflicting input raises LoopiterError(code="conflict"). Defaults omitted on the
first request must remain omitted on retries. Revisions protect explicit updates.
Namespaces are integrity scopes, not authentication. Verify correction sources,
select authorized namespaces, and sanitize sensitive content in your application.
An async sanitize(value) hook runs before persistence and must return valid JSON;
failure never falls back to unsanitized input. Defaults: 256 KiB per input and
120 seconds per async callback. Sanitizers must be deterministic for retries and
must preserve deployment identity fields. Loopiter sends no data anywhere except
your explicitly supplied store and callbacks.
PostgreSQL (optional)
python -m pip install 'loopiter[postgres]==0.2.0a1'
Review the packaged versioned migration before running it with an authorized setup identity. Constructors never execute migrations. Request-time credentials do not need schema-creation permissions. For example, during explicit setup only:
import asyncio
import os
from psycopg import AsyncConnection
from loopiter.postgres import migration_sql
async def setup():
# SQL contains its own BEGIN/COMMIT; execute on a standalone autocommit connection.
async with await AsyncConnection.connect(os.environ["DATABASE_URL"], autocommit=True) as conn:
await conn.execute(migration_sql())
if __name__ == "__main__":
asyncio.run(setup())
Runtime application:
import asyncio
import os
from psycopg_pool import AsyncConnectionPool
from loopiter import FeedbackLoop
from loopiter.postgres import PostgresStore
async def main():
async with AsyncConnectionPool(os.environ["DATABASE_URL"], open=False) as pool:
await pool.wait()
loop = FeedbackLoop(store=PostgresStore(pool), namespace="support/dev")
await loop.record_execution(kind="prediction", input={"text": "Synthetic check"})
await loop.close() # Does not close your pool; its context manager does.
if __name__ == "__main__":
asyncio.run(main())
Use TLS, timeouts, least-privilege DB credentials, backups and restore drills. The adapter uses one checked-out connection per transaction and namespace advisory locking across independent clients/processes. Follow psycopg's async pool lifecycle. No transaction is held during evaluation or deployment callbacks. Avoid nested store transactions, spawned tasks within a transaction, and direct SQL record mutations. These bypass or interfere with the adapter's guarantees.
Python uses contract v1, loopiter_python_records, separate migration/version
tables and lock keys. Node uses its existing contract v2 and tables. The languages
do not share records, hashes, active pointers, or deployment locks. They can use
the same PostgreSQL database, but must not independently control the same external
target. Choose one language as lifecycle owner and communicate through your own
application API if both languages participate in a single workflow. This alpha
does not provide a wire protocol, cross-language migration, or a shared service.
Evaluate, approve, deploy
- Analyze scored evidence with
await loop.analyze(dimensions=[...]). - Create an immutable proposal with
await loop.create_candidate(target={"kind": "prompt", "key": "support"}, proposed_change={...}, evidence={...}, risk="low"). Your application proposes it, using its own model or deterministic logic. evaluate_candidate(id, evaluator, evaluator_name=..., version=..., dataset_hash=...)calls your asyncevaluator(candidate, cancellation_event)outside the transaction. It must return{"passed": bool, "metrics": {"name": finite_number}}. Your evaluator must enforce independent holdout/guardrail thresholds; Loopiter does not invent them.- After human review, call
approve_candidate(id, actor=..., evaluation_id=...)with the exact latest passing evaluation's ID. Failed or stale evaluations cannot approve. - Explicitly call
deploy_candidate(id, adapter, expected_artifact_version=...). Omit the initial version only when your infrastructure really has no existing artifact. - Use
rollback_candidate(id, adapter)to restore its explicit predecessor/version.
Candidate IDs are insert-idempotency keys, not automatic semantic deduplication. For
proposal deduplication choose a stable ID derived with fingerprint({...}) from the
target, proposed change, evidence fingerprint, evaluator version and dataset hash.
Changed evidence/evaluator versions should produce a new ID. No callback runs
automatically after feedback capture. No Python controller or unattended auto-apply
ships in this first alpha.
Deployment and recovery contract
DeploymentAdapter exposes async apply(request), inspect(request), and
rollback(request). Each request contains attempt, candidate,
restore_candidate (or None), stable idempotency_key, and a cooperative
cancellation event. Your adapter must durably record attempt IDs, compare the
expected artifact version atomically, implement real rollback, and fence late calls.
Apply and rollback return:
receipt = {
"attempt_id": request["attempt"]["id"],
"artifact_version": "new-version", # Or None only when rollback removes an artifact.
"previous_artifact_version": "old-version", # Or None when genuinely absent.
}
Inspection returns {"status": "applied", "receipt": receipt}, {"status": "unknown"},
or {"status": "not_applied"}. not_applied means fenced: an earlier operation
cannot still complete later. A temporary absence or HTTP timeout is not proof.
Loopiter persists the attempt before the external change, then atomically finalizes
receipt, lifecycle events, candidate states and the active pointer. Ambiguous outcomes
stay pending and block the target. On LoopiterError with code deployment_pending,
use error.attempt_id with reconcile_deployment(attempt_id, adapter). On process
restart or task cancellation, paginate loop.list("attempts") and reconcile pending
attempts. Reconciliation only inspects; it never blindly repeats apply or rollback.
An unknown inspection stays pending. Repeated rollback cannot revive a rolled-back
version. This is not universal exactly-once execution.
Cancel a running SDK operation with task.cancel(). Cancellation/timeout discards
late callback results; it cannot sandbox code, kill a process or undo an external
change. Async callbacks must cooperate and must not block the event loop. Database
deadlines remain application-owned. deployments_enabled=lambda: False blocks new
applies, but rollback/reconciliation remain available. Pending attempts and lifecycle
events are available through paginated list; no logger, daemon, or scheduler is installed.
Analysis semantics
Separate execution_window and observation_window (inclusive from, exclusive to)
allow a later outcome to score an older execution. Include older executions explicitly
when narrowing the cohort. Default scoring recognizes numeric/boolean values and
success/failure words. Supply a finite-valued synchronous score(signal) and a
scoring_version for custom semantics. Conflicting corrections remain separate
evidence; they are never silently promoted to trusted labels.
Episodes are scoring units where available; otherwise executions are units. Signals deduplicate by ID within a unit; one episode outcome is not counted once per turn. Zero-confidence or unscored signals do not add effective support or recurrence. Counts are descriptive, not calibrated probabilities. Evidence includes a versioned fingerprint of scored values, execution revisions, baseline and scoring version. Default maximum is 20,000 executions and 20,000 signals per analysis; exceeding it raises an explicit error. Pages max at 1,000. Analysis materializes that bounded snapshot in memory; it is not a streaming warehouse engine.
Scope and release checks
Python includes capture, updates, structured analysis, manual candidate lifecycle, timeouts/cancellation, events, namespace deletion, an in-memory dev store, optional PostgreSQL 16/17 adapter, conformance tests and an offline end-to-end example. The Node controller/experimental auto-apply, JsonFileStore/legacy migration CLI, and OpenAI/Azure classification starter remain Node-only. Neither SDK automatically fine-tunes a model. Application callbacks, database permissions and deployment fencing remain application responsibilities.
From the repository root:
python -m pip install './python[dev,postgres]'
python -m unittest discover -s python/tests -v
ruff check python scripts/python-package-smoke.py
ruff format --check python scripts/python-package-smoke.py
python -m build python
twine check python/dist/*
python scripts/python-package-smoke.py
Set LOOPITER_PYTHON_TEST_DATABASE_URL only to a disposable PostgreSQL database;
integration tests explicitly migrate and delete only their randomly named test
namespaces. CI runs Python 3.11–3.14 against PostgreSQL 16/17. Maintainers publish
through the manual python-publish.yml workflow with an exact version/commit,
all validation jobs passing, and the protected pypi environment approved. The
uploaded wheel/sdist are the same artifacts installed in clean-consumer tests.
Trusted Publishing uses short-lived GitHub identity; no release credentials belong
in this repository. See the release procedure.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file loopiter-0.2.0a1.tar.gz.
File metadata
- Download URL: loopiter-0.2.0a1.tar.gz
- Upload date:
- Size: 32.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
db088fdef144a1f90b6a0fe1e54cdc2acbdad82080ce1b2a0f80fe661e58cd81
|
|
| MD5 |
444d3eb0b5423e14ce330c02a668f97e
|
|
| BLAKE2b-256 |
70df0d7d5d7275040c6463ec79cb136b4abd47f8155fd920a4ff934da18ed77f
|
Provenance
The following attestation bundles were made for loopiter-0.2.0a1.tar.gz:
Publisher:
python-publish.yml on SanaanKhalid/loopiter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
loopiter-0.2.0a1.tar.gz -
Subject digest:
db088fdef144a1f90b6a0fe1e54cdc2acbdad82080ce1b2a0f80fe661e58cd81 - Sigstore transparency entry: 2812003959
- Sigstore integration time:
-
Permalink:
SanaanKhalid/loopiter@891542a82cb1b4333384b25f81c8e88381fe047a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/SanaanKhalid
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@891542a82cb1b4333384b25f81c8e88381fe047a -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file loopiter-0.2.0a1-py3-none-any.whl.
File metadata
- Download URL: loopiter-0.2.0a1-py3-none-any.whl
- Upload date:
- Size: 27.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1ab0cbbe1ec4cc280137cd702a5bd6cecc5d96419cc237b7e763f34daf24cea9
|
|
| MD5 |
7c9d80f4bdb070987b93b901fba2b633
|
|
| BLAKE2b-256 |
9fd9ad024d41da1654534b8439c6730007245b8a1a646799c3ad735d7f1a9dc2
|
Provenance
The following attestation bundles were made for loopiter-0.2.0a1-py3-none-any.whl:
Publisher:
python-publish.yml on SanaanKhalid/loopiter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
loopiter-0.2.0a1-py3-none-any.whl -
Subject digest:
1ab0cbbe1ec4cc280137cd702a5bd6cecc5d96419cc237b7e763f34daf24cea9 - Sigstore transparency entry: 2812004052
- Sigstore integration time:
-
Permalink:
SanaanKhalid/loopiter@891542a82cb1b4333384b25f81c8e88381fe047a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/SanaanKhalid
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@891542a82cb1b4333384b25f81c8e88381fe047a -
Trigger Event:
workflow_dispatch
-
Statement type: