python-corekit 0.3.0__tar.gz → 0.4.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.
- {python_corekit-0.3.0/python_corekit.egg-info → python_corekit-0.4.0}/PKG-INFO +33 -4
- {python_corekit-0.3.0 → python_corekit-0.4.0}/README.md +30 -2
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/__init__.py +5 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/client.py +34 -3
- python_corekit-0.4.0/corekit/http/stream.py +110 -0
- python_corekit-0.4.0/corekit/llm/__init__.py +134 -0
- python_corekit-0.4.0/corekit/llm/client.py +179 -0
- python_corekit-0.4.0/corekit/llm/enum.py +123 -0
- python_corekit-0.4.0/corekit/llm/events.py +96 -0
- python_corekit-0.4.0/corekit/llm/messages.py +173 -0
- python_corekit-0.4.0/corekit/llm/prompts/__init__.py +19 -0
- python_corekit-0.4.0/corekit/llm/prompts/enum.py +54 -0
- python_corekit-0.4.0/corekit/llm/prompts/exceptions.py +22 -0
- python_corekit-0.4.0/corekit/llm/prompts/loader.py +139 -0
- python_corekit-0.4.0/corekit/llm/prompts/template.py +53 -0
- python_corekit-0.4.0/corekit/llm/protocols.py +65 -0
- python_corekit-0.4.0/corekit/llm/streaming.py +149 -0
- python_corekit-0.4.0/corekit/llm/tools/__init__.py +19 -0
- python_corekit-0.4.0/corekit/llm/tools/base.py +118 -0
- python_corekit-0.4.0/corekit/llm/tools/detection.py +99 -0
- python_corekit-0.4.0/corekit/llm/tools/loop.py +255 -0
- python_corekit-0.4.0/corekit/llm/tools/registry.py +103 -0
- python_corekit-0.4.0/corekit/llm/wire.py +199 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/__init__.py +3 -3
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/benchmarkable.py +16 -2
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/timing/split.py +14 -0
- python_corekit-0.4.0/corekit/observability/timing/timer.py +54 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/__init__.py +2 -1
- python_corekit-0.4.0/corekit/schemas/version.py +58 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/__init__.py +2 -1
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/collections.py +16 -1
- {python_corekit-0.3.0 → python_corekit-0.4.0}/pyproject.toml +5 -2
- {python_corekit-0.3.0 → python_corekit-0.4.0/python_corekit.egg-info}/PKG-INFO +33 -4
- {python_corekit-0.3.0 → python_corekit-0.4.0}/python_corekit.egg-info/SOURCES.txt +21 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/python_corekit.egg-info/requires.txt +1 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_architecture.py +1 -0
- python_corekit-0.4.0/tests/test_llm.py +492 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_observability.py +24 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_requests.py +133 -1
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_schemas_enum.py +12 -1
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils.py +15 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_collections.py +11 -0
- python_corekit-0.3.0/corekit/observability/timing/timer.py +0 -32
- {python_corekit-0.3.0 → python_corekit-0.4.0}/LICENSE +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/application.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/handler.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/lifespan.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/middleware.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/responses.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/api/routers.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/concurrency/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/concurrency/decorators.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/concurrency/thread_local.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/concurrency/worker.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/config/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/config/loader.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/config/settings.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/config/sources.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/connectable.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/decorators.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/redis/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/redis/connection.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/registry.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/connection.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/fields/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/fields/jsonb.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/base.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/operations.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/registry.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/migration/table.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/base.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/operations/statements.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/query.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/connections/sql/table.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/crypto/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/crypto/constants.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/crypto/enum.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/crypto/hasher.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/dataset.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/comparison.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/expression.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/operator.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/expressions/target.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/record.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/data/stats.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/decorators/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/decorators/exception_handling.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/decorators/warnings.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/docker/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/docker/watchdog.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/connection.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/extract/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/extract/extractor.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/extract/schemas.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/load/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/load/loader.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/load/schemas.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/orchestrator.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/schemas.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/transform/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/transform/schemas.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/etl/transform/transformer.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/enum.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/frames.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/models.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/publisher.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/reader.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/sse.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/events/websocket.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/base.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/custom/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/enum.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/http/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/exceptions/types.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/base.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/enum.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/json.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/files/toml.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/api.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/exceptions.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/exponential_backoff.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/response.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/http/status.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/jobs/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/jobs/registry.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/jobs/runner.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/jobs/task.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/log_monitor/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/log_monitor/constants.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/log_monitor/models.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/log_monitor/service.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/notifications/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/notifications/base.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/notifications/models.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/loggable.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/request_context.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/timing/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/observability/timing/constants.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/py.typed +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/registry/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/registry/ordered.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/registry/registry.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/dataclasses/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/enum.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/models/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/models/arbitrary.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/models/date_models.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/pydantic/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/pydantic/fields.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/schemas/types.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/__init__.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/enum.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/pickle_file.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/serializable.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/serialization/serializer.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/coercion.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/ids.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/payload.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/raise_exc.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/text.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/time.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/validators.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/corekit/utils/void.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/python_corekit.egg-info/dependency_links.txt +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/python_corekit.egg-info/top_level.txt +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/setup.cfg +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/setup.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_api.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_application.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_concurrency.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_config.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_connections.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_data.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_docker.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_etl.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_events.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_exceptions.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_expressions.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_expressions_sqlalchemy.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_files.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_imports.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_imports_are_top_level.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_jobs.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_lifespan.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_log_monitor.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_middleware_stack.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_migration.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_notifications.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_operations.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_ordered_registry.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_redis.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_registry.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_request_context.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_serialization.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_sql.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_coercion.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_ids.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_payload.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_text.py +0 -0
- {python_corekit-0.3.0 → python_corekit-0.4.0}/tests/test_utils_time.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-corekit
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, and
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Shared foundations for Python projects: logging, benchmarking, registries, FastAPI application and routers, SQL statements and migrations, background tasks, ETL, and LLM chat/tool primitives
|
|
5
5
|
Author: Steven Jacobsen
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/stevejaker/corekit
|
|
@@ -18,6 +18,7 @@ License-File: LICENSE
|
|
|
18
18
|
Requires-Dist: pydantic<3,>=2.10
|
|
19
19
|
Requires-Dist: pydantic-settings<3,>=2.0
|
|
20
20
|
Requires-Dist: fastapi<1,>=0.115
|
|
21
|
+
Requires-Dist: starlette<0.47,>=0.40
|
|
21
22
|
Requires-Dist: sqlmodel<0.1,>=0.0.16
|
|
22
23
|
Requires-Dist: SQLAlchemy<3,>=2.0
|
|
23
24
|
Requires-Dist: redis<7,>=5.0
|
|
@@ -37,7 +38,8 @@ Dynamic: license-file
|
|
|
37
38
|
|
|
38
39
|
Shared foundations for Python projects: structured logging, benchmarking,
|
|
39
40
|
registries, a FastAPI application with routers and handlers, SQL statements and
|
|
40
|
-
migrations, background tasks, an in-memory record store,
|
|
41
|
+
migrations, background tasks, an in-memory record store, ETL scaffolding, and
|
|
42
|
+
OpenAI-shaped chat / tool-loop primitives.
|
|
41
43
|
|
|
42
44
|
Requires Python 3.11+.
|
|
43
45
|
|
|
@@ -55,7 +57,7 @@ extras to remember, and no import that fails because something was left out.
|
|
|
55
57
|
Pin a compatible release rather than tracking whatever is newest:
|
|
56
58
|
|
|
57
59
|
```
|
|
58
|
-
python-corekit~=0.
|
|
60
|
+
python-corekit~=0.4.0
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
Before 1.0, the minor version carries breaking changes.
|
|
@@ -281,6 +283,32 @@ ceiling, not 9,999 threads.
|
|
|
281
283
|
Failures propagate by default. Pass `raise_on_error=False` to log and skip them
|
|
282
284
|
instead, which loses results silently and so is opt-in.
|
|
283
285
|
|
|
286
|
+
## LLM chat and tools
|
|
287
|
+
|
|
288
|
+
OpenAI-shaped messages, a tool registry, an agentic loop, and a thin
|
|
289
|
+
``ChatCompletionsClient`` (a ``BaseApiClient``) for any OpenAI-compatible
|
|
290
|
+
server — llama.cpp, vLLM, OpenAI, and so on. Not a multi-provider generation
|
|
291
|
+
facade.
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
|
|
295
|
+
|
|
296
|
+
client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
|
|
297
|
+
prompts = PromptLoader("/path/to/prompts") # mount any compatible tree
|
|
298
|
+
tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
|
|
299
|
+
|
|
300
|
+
registry = ToolRegistry()
|
|
301
|
+
registry.register(MyTool())
|
|
302
|
+
loop = ToolLoop(client, registry)
|
|
303
|
+
async for event in loop.stream(tmpl.messages(text="…", style="brief", length="short")):
|
|
304
|
+
...
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
``PromptLoader`` reads versioned ``.txt`` files from a directory you point at
|
|
308
|
+
(``{kind}/{name}/{role}-v{version}.txt``, plus ``common/`` fragments). Prompt
|
|
309
|
+
*text* stays with the consumer — corekit only ships the loader. See
|
|
310
|
+
`docs/LLM_EXTRACTION.md`.
|
|
311
|
+
|
|
284
312
|
## HTTP clients
|
|
285
313
|
|
|
286
314
|
```python
|
|
@@ -394,6 +422,7 @@ corekit/
|
|
|
394
422
|
|
|
395
423
|
api/ Application, lifespan, middleware, routers
|
|
396
424
|
docker/ notifications/ etl/
|
|
425
|
+
llm/ messages, tools, agentic loop (no GenerationClient)
|
|
397
426
|
|
|
398
427
|
events/ log_monitor/ built on the capabilities above
|
|
399
428
|
```
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Shared foundations for Python projects: structured logging, benchmarking,
|
|
4
4
|
registries, a FastAPI application with routers and handlers, SQL statements and
|
|
5
|
-
migrations, background tasks, an in-memory record store,
|
|
5
|
+
migrations, background tasks, an in-memory record store, ETL scaffolding, and
|
|
6
|
+
OpenAI-shaped chat / tool-loop primitives.
|
|
6
7
|
|
|
7
8
|
Requires Python 3.11+.
|
|
8
9
|
|
|
@@ -20,7 +21,7 @@ extras to remember, and no import that fails because something was left out.
|
|
|
20
21
|
Pin a compatible release rather than tracking whatever is newest:
|
|
21
22
|
|
|
22
23
|
```
|
|
23
|
-
python-corekit~=0.
|
|
24
|
+
python-corekit~=0.4.0
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
Before 1.0, the minor version carries breaking changes.
|
|
@@ -246,6 +247,32 @@ ceiling, not 9,999 threads.
|
|
|
246
247
|
Failures propagate by default. Pass `raise_on_error=False` to log and skip them
|
|
247
248
|
instead, which loses results silently and so is opt-in.
|
|
248
249
|
|
|
250
|
+
## LLM chat and tools
|
|
251
|
+
|
|
252
|
+
OpenAI-shaped messages, a tool registry, an agentic loop, and a thin
|
|
253
|
+
``ChatCompletionsClient`` (a ``BaseApiClient``) for any OpenAI-compatible
|
|
254
|
+
server — llama.cpp, vLLM, OpenAI, and so on. Not a multi-provider generation
|
|
255
|
+
facade.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
|
|
259
|
+
|
|
260
|
+
client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
|
|
261
|
+
prompts = PromptLoader("/path/to/prompts") # mount any compatible tree
|
|
262
|
+
tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
|
|
263
|
+
|
|
264
|
+
registry = ToolRegistry()
|
|
265
|
+
registry.register(MyTool())
|
|
266
|
+
loop = ToolLoop(client, registry)
|
|
267
|
+
async for event in loop.stream(tmpl.messages(text="…", style="brief", length="short")):
|
|
268
|
+
...
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
``PromptLoader`` reads versioned ``.txt`` files from a directory you point at
|
|
272
|
+
(``{kind}/{name}/{role}-v{version}.txt``, plus ``common/`` fragments). Prompt
|
|
273
|
+
*text* stays with the consumer — corekit only ships the loader. See
|
|
274
|
+
`docs/LLM_EXTRACTION.md`.
|
|
275
|
+
|
|
249
276
|
## HTTP clients
|
|
250
277
|
|
|
251
278
|
```python
|
|
@@ -359,6 +386,7 @@ corekit/
|
|
|
359
386
|
|
|
360
387
|
api/ Application, lifespan, middleware, routers
|
|
361
388
|
docker/ notifications/ etl/
|
|
389
|
+
llm/ messages, tools, agentic loop (no GenerationClient)
|
|
362
390
|
|
|
363
391
|
events/ log_monitor/ built on the capabilities above
|
|
364
392
|
```
|
|
@@ -27,6 +27,7 @@ from corekit.http.exceptions import (
|
|
|
27
27
|
from corekit.http.exponential_backoff import ExponentialBackoff
|
|
28
28
|
from corekit.http.response import BaseApiResponse
|
|
29
29
|
from corekit.http.status import HTTPStatusCode
|
|
30
|
+
from corekit.http.stream import SseDecoder, SseJsonDecoder, SseTextDecoder, StreamDecoder
|
|
30
31
|
|
|
31
32
|
__all__ = [
|
|
32
33
|
"BadGatewayException",
|
|
@@ -42,6 +43,10 @@ __all__ = [
|
|
|
42
43
|
"InternalServerErrorException",
|
|
43
44
|
"NotFoundException",
|
|
44
45
|
"ServiceUnavailableException",
|
|
46
|
+
"SseDecoder",
|
|
47
|
+
"SseJsonDecoder",
|
|
48
|
+
"SseTextDecoder",
|
|
49
|
+
"StreamDecoder",
|
|
45
50
|
"TooManyRequestsException",
|
|
46
51
|
"UnauthorizedException",
|
|
47
52
|
"UnprocessableEntityException",
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
A small HTTP client with retries.
|
|
3
3
|
|
|
4
4
|
``BaseHttpClient`` is the transport: relative paths, optional strict URL
|
|
5
|
-
checking,
|
|
6
|
-
retryable
|
|
7
|
-
|
|
5
|
+
checking, retries for statuses the typed HTTP exceptions mark as
|
|
6
|
+
retryable, and ``stream_request`` for SSE / chunked bodies. Subclass
|
|
7
|
+
``BaseApiClient`` when the client always talks to one API and a foreign
|
|
8
|
+
absolute URL should be a bug.
|
|
8
9
|
|
|
9
10
|
class GithubClient(BaseApiClient):
|
|
10
11
|
'''
|
|
@@ -27,6 +28,8 @@ Every response comes back as a ``BaseApiResponse``, so callers see one shape
|
|
|
27
28
|
regardless of what the endpoint returned.
|
|
28
29
|
"""
|
|
29
30
|
|
|
31
|
+
from collections.abc import AsyncIterator
|
|
32
|
+
from contextlib import asynccontextmanager
|
|
30
33
|
from http import HTTPMethod
|
|
31
34
|
from typing import Any
|
|
32
35
|
from urllib.parse import urljoin, urlsplit, urlunsplit
|
|
@@ -182,6 +185,34 @@ class BaseHttpClient(Benchmarkable):
|
|
|
182
185
|
self.warning(f"{method} {_without_query(target)} returned {raw.status_code}; retrying")
|
|
183
186
|
await backoff.async_wait()
|
|
184
187
|
|
|
188
|
+
@asynccontextmanager
|
|
189
|
+
async def stream_request(self, method: HTTPMethod, url: str, **kwargs: Any) -> AsyncIterator[httpx.Response]:
|
|
190
|
+
"""
|
|
191
|
+
Open a streaming response and yield the raw ``httpx.Response``.
|
|
192
|
+
|
|
193
|
+
Holds the connection for the life of the ``async with`` block. Unlike
|
|
194
|
+
``request``, the body is not buffered into a ``BaseApiResponse`` — the
|
|
195
|
+
caller reads it (for example with ``aiter_lines`` for SSE). Retries are
|
|
196
|
+
not applied: a mid-stream failure cannot be replayed safely.
|
|
197
|
+
"""
|
|
198
|
+
if method not in self.supported_methods:
|
|
199
|
+
raise UnsupportedMethodError(method)
|
|
200
|
+
|
|
201
|
+
target = self._build_url(url)
|
|
202
|
+
headers = dict(self.headers)
|
|
203
|
+
extra_headers: dict[str, Any] | None = kwargs.pop("headers", None)
|
|
204
|
+
if extra_headers:
|
|
205
|
+
headers.update(extra_headers)
|
|
206
|
+
|
|
207
|
+
if self._http_client is not None:
|
|
208
|
+
async with self._http_client.stream(str(method), target, headers=headers, **kwargs) as response:
|
|
209
|
+
yield response
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
213
|
+
async with client.stream(str(method), target, headers=headers, **kwargs) as response:
|
|
214
|
+
yield response
|
|
215
|
+
|
|
185
216
|
@allow_sync
|
|
186
217
|
async def get(self, url: str, **kwargs: Any) -> BaseApiResponse:
|
|
187
218
|
return await self.request(HTTPMethod.GET, url, **kwargs)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decode streaming HTTP response bodies into typed events.
|
|
3
|
+
|
|
4
|
+
``BaseHttpClient.stream_request`` yields a raw ``httpx.Response``. Decoders
|
|
5
|
+
turn that into an async iterator of useful values (SSE text, SSE JSON, …).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from collections.abc import AsyncIterator
|
|
13
|
+
from typing import Any, Generic, TypeVar
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from corekit.observability import Loggable
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"SseDecoder",
|
|
21
|
+
"SseJsonDecoder",
|
|
22
|
+
"SseTextDecoder",
|
|
23
|
+
"StreamDecoder",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StreamDecoder(Loggable, ABC, Generic[T]):
|
|
30
|
+
"""
|
|
31
|
+
Turn a streaming ``httpx.Response`` into an async iterator of ``T``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def decode(self, response: httpx.Response) -> AsyncIterator[T]:
|
|
36
|
+
"""
|
|
37
|
+
Consume ``response`` and yield decoded items.
|
|
38
|
+
|
|
39
|
+
Implemented as an async generator on concrete subclasses.
|
|
40
|
+
"""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SseDecoder(StreamDecoder[T], ABC):
|
|
45
|
+
"""
|
|
46
|
+
Server-Sent Events framing: ``data:`` lines, skip comments / blanks.
|
|
47
|
+
|
|
48
|
+
Optional ``done_sentinel`` (e.g. OpenAI's ``[DONE]``) ends the stream
|
|
49
|
+
early. Subclasses interpret each data payload via ``_decode_payload``.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, *, done_sentinel: str | None = None) -> None:
|
|
53
|
+
super().__init__()
|
|
54
|
+
self.done_sentinel = done_sentinel
|
|
55
|
+
|
|
56
|
+
async def decode(self, response: httpx.Response) -> AsyncIterator[T]:
|
|
57
|
+
async for data in self._iter_data_payloads(response):
|
|
58
|
+
item = self._decode_payload(data)
|
|
59
|
+
if item is not None:
|
|
60
|
+
yield item
|
|
61
|
+
|
|
62
|
+
async def _iter_data_payloads(self, response: httpx.Response) -> AsyncIterator[str]:
|
|
63
|
+
async for line in response.aiter_lines():
|
|
64
|
+
line = line.strip()
|
|
65
|
+
if not line.startswith("data:"):
|
|
66
|
+
continue
|
|
67
|
+
data = line.removeprefix("data:").strip()
|
|
68
|
+
if not data:
|
|
69
|
+
continue
|
|
70
|
+
if self.done_sentinel is not None and data == self.done_sentinel:
|
|
71
|
+
return
|
|
72
|
+
yield data
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def _decode_payload(self, data: str) -> T | None:
|
|
76
|
+
"""
|
|
77
|
+
Map one SSE data payload to ``T``, or ``None`` to skip.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class SseTextDecoder(SseDecoder[str]):
|
|
82
|
+
"""
|
|
83
|
+
Yield SSE data payloads as plain strings.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def _decode_payload(self, data: str) -> str | None:
|
|
87
|
+
return data
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SseJsonDecoder(SseDecoder[dict[str, Any]]):
|
|
91
|
+
"""
|
|
92
|
+
Yield SSE data payloads parsed as JSON objects.
|
|
93
|
+
|
|
94
|
+
Non-object JSON and decode failures are skipped (with a warning). Default
|
|
95
|
+
``done_sentinel`` matches OpenAI-compatible chat streams.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, *, done_sentinel: str | None = "[DONE]") -> None:
|
|
99
|
+
super().__init__(done_sentinel=done_sentinel)
|
|
100
|
+
|
|
101
|
+
def _decode_payload(self, data: str) -> dict[str, Any] | None:
|
|
102
|
+
try:
|
|
103
|
+
payload = json.loads(data)
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
self.warning(f"[SseJsonDecoder] Skipping bad SSE payload: {data!r}")
|
|
106
|
+
return None
|
|
107
|
+
if isinstance(payload, dict):
|
|
108
|
+
return payload
|
|
109
|
+
self.warning(f"[SseJsonDecoder] Skipping non-object SSE JSON: {type(payload).__name__}")
|
|
110
|
+
return None
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI-shaped chat primitives, tool registry, and agentic loop.
|
|
3
|
+
|
|
4
|
+
Wire format in, structured stream events out. Use ``ChatCompletionsClient``
|
|
5
|
+
against any OpenAI-compatible server (llama.cpp, vLLM, OpenAI, …):
|
|
6
|
+
|
|
7
|
+
from corekit.llm import ChatCompletionsClient, PromptLoader, ToolLoop, ToolRegistry
|
|
8
|
+
|
|
9
|
+
client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
|
|
10
|
+
prompts = PromptLoader("/path/to/prompts")
|
|
11
|
+
tmpl = prompts.load("summarization", "general", system="1.0.0", user="1.0.0")
|
|
12
|
+
registry = ToolRegistry()
|
|
13
|
+
registry.register(MyTool())
|
|
14
|
+
loop = ToolLoop(client, registry)
|
|
15
|
+
async for event in loop.stream(tmpl.messages(text="…", style="brief", length="1 paragraph")):
|
|
16
|
+
...
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from corekit.llm.client import DEFAULT_LLM_TIMEOUT, ChatCompletionsClient
|
|
20
|
+
from corekit.llm.enum import (
|
|
21
|
+
ContentPartType,
|
|
22
|
+
FinishReason,
|
|
23
|
+
Role,
|
|
24
|
+
StopReason,
|
|
25
|
+
StreamEventType,
|
|
26
|
+
ToolCallType,
|
|
27
|
+
ToolKind,
|
|
28
|
+
WireField,
|
|
29
|
+
)
|
|
30
|
+
from corekit.llm.events import (
|
|
31
|
+
DoneEvent,
|
|
32
|
+
StopEvent,
|
|
33
|
+
StreamEvent,
|
|
34
|
+
TextEvent,
|
|
35
|
+
ThinkingEvent,
|
|
36
|
+
ToolCallEvent,
|
|
37
|
+
ToolResultEvent,
|
|
38
|
+
UIComponentEvent,
|
|
39
|
+
UsageEvent,
|
|
40
|
+
)
|
|
41
|
+
from corekit.llm.messages import (
|
|
42
|
+
AssistantMessage,
|
|
43
|
+
ChatTurn,
|
|
44
|
+
Message,
|
|
45
|
+
SystemMessage,
|
|
46
|
+
ToolMessage,
|
|
47
|
+
UserMessage,
|
|
48
|
+
)
|
|
49
|
+
from corekit.llm.prompts import (
|
|
50
|
+
DEFAULT_PROMPT_VERSION,
|
|
51
|
+
PromptError,
|
|
52
|
+
PromptFileRole,
|
|
53
|
+
PromptKind,
|
|
54
|
+
PromptLoader,
|
|
55
|
+
PromptNotFoundError,
|
|
56
|
+
PromptTemplate,
|
|
57
|
+
ThinkingLevel,
|
|
58
|
+
)
|
|
59
|
+
from corekit.llm.protocols import ChatClient, MetricsRecorder
|
|
60
|
+
from corekit.llm.streaming import (
|
|
61
|
+
THINK_PATTERN,
|
|
62
|
+
ModelStreamingState,
|
|
63
|
+
ThinkingMarker,
|
|
64
|
+
ThinkingMarkerPair,
|
|
65
|
+
strip_thinking,
|
|
66
|
+
)
|
|
67
|
+
from corekit.llm.tools.base import Tool, ToolCall, ToolResult
|
|
68
|
+
from corekit.llm.tools.detection import MAX_TOOL_ITERATIONS, ToolCallLoopDetector
|
|
69
|
+
from corekit.llm.tools.loop import ToolLoop
|
|
70
|
+
from corekit.llm.tools.registry import ToolRegistry
|
|
71
|
+
from corekit.llm.wire import (
|
|
72
|
+
ChoiceDelta,
|
|
73
|
+
CompletionTurn,
|
|
74
|
+
FunctionCallDelta,
|
|
75
|
+
ToolCallAccumulator,
|
|
76
|
+
ToolCallDelta,
|
|
77
|
+
UsageInfo,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"DEFAULT_LLM_TIMEOUT",
|
|
82
|
+
"DEFAULT_PROMPT_VERSION",
|
|
83
|
+
"MAX_TOOL_ITERATIONS",
|
|
84
|
+
"THINK_PATTERN",
|
|
85
|
+
"AssistantMessage",
|
|
86
|
+
"ChatClient",
|
|
87
|
+
"ChatCompletionsClient",
|
|
88
|
+
"ChatTurn",
|
|
89
|
+
"ChoiceDelta",
|
|
90
|
+
"CompletionTurn",
|
|
91
|
+
"ContentPartType",
|
|
92
|
+
"DoneEvent",
|
|
93
|
+
"FinishReason",
|
|
94
|
+
"FunctionCallDelta",
|
|
95
|
+
"Message",
|
|
96
|
+
"MetricsRecorder",
|
|
97
|
+
"ModelStreamingState",
|
|
98
|
+
"PromptError",
|
|
99
|
+
"PromptFileRole",
|
|
100
|
+
"PromptKind",
|
|
101
|
+
"PromptLoader",
|
|
102
|
+
"PromptNotFoundError",
|
|
103
|
+
"PromptTemplate",
|
|
104
|
+
"Role",
|
|
105
|
+
"StopEvent",
|
|
106
|
+
"StopReason",
|
|
107
|
+
"StreamEvent",
|
|
108
|
+
"StreamEventType",
|
|
109
|
+
"SystemMessage",
|
|
110
|
+
"TextEvent",
|
|
111
|
+
"ThinkingEvent",
|
|
112
|
+
"ThinkingLevel",
|
|
113
|
+
"ThinkingMarker",
|
|
114
|
+
"ThinkingMarkerPair",
|
|
115
|
+
"Tool",
|
|
116
|
+
"ToolCall",
|
|
117
|
+
"ToolCallAccumulator",
|
|
118
|
+
"ToolCallDelta",
|
|
119
|
+
"ToolCallEvent",
|
|
120
|
+
"ToolCallLoopDetector",
|
|
121
|
+
"ToolCallType",
|
|
122
|
+
"ToolKind",
|
|
123
|
+
"ToolLoop",
|
|
124
|
+
"ToolMessage",
|
|
125
|
+
"ToolRegistry",
|
|
126
|
+
"ToolResult",
|
|
127
|
+
"ToolResultEvent",
|
|
128
|
+
"UIComponentEvent",
|
|
129
|
+
"UsageEvent",
|
|
130
|
+
"UsageInfo",
|
|
131
|
+
"UserMessage",
|
|
132
|
+
"WireField",
|
|
133
|
+
"strip_thinking",
|
|
134
|
+
]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI-compatible chat completions client.
|
|
3
|
+
|
|
4
|
+
A thin ``BaseApiClient`` for ``/chat/completions``. Transport and status
|
|
5
|
+
handling come from the HTTP layer; streaming bodies go through
|
|
6
|
+
``SseJsonDecoder``. This module only knows the OpenAI request wire shape.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import AsyncIterator, Coroutine, Mapping, Sequence
|
|
12
|
+
from http import HTTPMethod
|
|
13
|
+
from typing import Any, Literal, overload
|
|
14
|
+
|
|
15
|
+
from corekit.concurrency.decorators import allow_sync
|
|
16
|
+
from corekit.http import BaseApiClient, BaseApiResponse, SseJsonDecoder
|
|
17
|
+
from corekit.llm.messages import ChatTurn, Message
|
|
18
|
+
|
|
19
|
+
__all__ = ["ChatCompletionsClient", "DEFAULT_LLM_TIMEOUT"]
|
|
20
|
+
|
|
21
|
+
# LLM calls can run for minutes; the HTTP default of 30s is too short.
|
|
22
|
+
DEFAULT_LLM_TIMEOUT = 600.0
|
|
23
|
+
|
|
24
|
+
# Owned by this client — sampling kwargs and friends pass through untouched.
|
|
25
|
+
_BODY_RESERVED = frozenset({"model", "messages", "stream", "stream_options", "tools", "tool_choice"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ChatCompletionsClient(BaseApiClient):
|
|
29
|
+
"""
|
|
30
|
+
OpenAI-shaped chat completions over HTTP.
|
|
31
|
+
|
|
32
|
+
client = ChatCompletionsClient("http://localhost:8080/v1", model="local")
|
|
33
|
+
|
|
34
|
+
# one-shot: a dict from sync code, a coroutine inside a running loop
|
|
35
|
+
result = client.complete([UserMessage(content="hi")])
|
|
36
|
+
result = await client.complete([UserMessage(content="hi")])
|
|
37
|
+
|
|
38
|
+
# streaming is always an async iterator — there is no sync equivalent
|
|
39
|
+
async for chunk in client.complete(messages, stream=True):
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
``base_url`` should include the API root (typically ``…/v1``). Accepts
|
|
43
|
+
``Message`` instances or raw OpenAI message dicts.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
base_url: str,
|
|
49
|
+
*,
|
|
50
|
+
api_key: str | None = None,
|
|
51
|
+
model: str | None = None,
|
|
52
|
+
timeout: float = DEFAULT_LLM_TIMEOUT,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._api_key = api_key
|
|
56
|
+
self._model = model
|
|
57
|
+
# Trailing slash so urljoin keeps the ``/v1`` segment for relative paths.
|
|
58
|
+
self._api_base = base_url if base_url.endswith("/") else f"{base_url}/"
|
|
59
|
+
self._stream_decoder = SseJsonDecoder()
|
|
60
|
+
super().__init__(timeout=timeout, **kwargs)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def base_url(self) -> str:
|
|
64
|
+
return self._api_base
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def headers(self) -> dict[str, str]:
|
|
68
|
+
if not self._api_key:
|
|
69
|
+
return {}
|
|
70
|
+
return {"Authorization": f"Bearer {self._api_key}"}
|
|
71
|
+
|
|
72
|
+
def _resolve_model(self, model: str | None) -> str:
|
|
73
|
+
resolved = model if model is not None else self._model
|
|
74
|
+
if not resolved:
|
|
75
|
+
raise ValueError("model is required (pass model= to complete() or the constructor)")
|
|
76
|
+
return resolved
|
|
77
|
+
|
|
78
|
+
def _build_body(
|
|
79
|
+
self,
|
|
80
|
+
messages: Sequence[ChatTurn],
|
|
81
|
+
*,
|
|
82
|
+
stream: bool,
|
|
83
|
+
tools: Sequence[Mapping[str, Any]] | None = None,
|
|
84
|
+
model: str | None = None,
|
|
85
|
+
internal: bool = False,
|
|
86
|
+
**params: Any,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
body: dict[str, Any] = {
|
|
89
|
+
"model": self._resolve_model(model),
|
|
90
|
+
"messages": Message.as_wire(messages, internal=internal),
|
|
91
|
+
"stream": stream,
|
|
92
|
+
}
|
|
93
|
+
if stream:
|
|
94
|
+
body["stream_options"] = {"include_usage": True}
|
|
95
|
+
if tools:
|
|
96
|
+
body["tools"] = list(tools)
|
|
97
|
+
body["tool_choice"] = params.pop("tool_choice", "auto")
|
|
98
|
+
for key, value in params.items():
|
|
99
|
+
if key not in _BODY_RESERVED:
|
|
100
|
+
body[key] = value
|
|
101
|
+
return body
|
|
102
|
+
|
|
103
|
+
@overload
|
|
104
|
+
def complete(
|
|
105
|
+
self,
|
|
106
|
+
messages: Sequence[ChatTurn],
|
|
107
|
+
*,
|
|
108
|
+
tools: Sequence[Mapping[str, Any]] | None = None,
|
|
109
|
+
model: str | None = None,
|
|
110
|
+
stream: Literal[False] = False,
|
|
111
|
+
internal: bool = False,
|
|
112
|
+
**kwargs: Any,
|
|
113
|
+
) -> dict[str, Any] | Coroutine[Any, Any, dict[str, Any]]: ...
|
|
114
|
+
|
|
115
|
+
@overload
|
|
116
|
+
def complete(
|
|
117
|
+
self,
|
|
118
|
+
messages: Sequence[ChatTurn],
|
|
119
|
+
*,
|
|
120
|
+
tools: Sequence[Mapping[str, Any]] | None = None,
|
|
121
|
+
model: str | None = None,
|
|
122
|
+
stream: Literal[True],
|
|
123
|
+
internal: bool = False,
|
|
124
|
+
**kwargs: Any,
|
|
125
|
+
) -> AsyncIterator[dict[str, Any]]: ...
|
|
126
|
+
|
|
127
|
+
def complete(
|
|
128
|
+
self,
|
|
129
|
+
messages: Sequence[ChatTurn],
|
|
130
|
+
*,
|
|
131
|
+
tools: Sequence[Mapping[str, Any]] | None = None,
|
|
132
|
+
model: str | None = None,
|
|
133
|
+
stream: bool = False,
|
|
134
|
+
internal: bool = False,
|
|
135
|
+
**kwargs: Any,
|
|
136
|
+
) -> dict[str, Any] | Coroutine[Any, Any, dict[str, Any]] | AsyncIterator[dict[str, Any]]:
|
|
137
|
+
"""
|
|
138
|
+
POST ``/chat/completions``.
|
|
139
|
+
|
|
140
|
+
``stream=False`` (default) uses ``@allow_sync``: a script gets the
|
|
141
|
+
response dict back; inside a running event loop you must ``await`` it.
|
|
142
|
+
``stream=True`` is always an async iterator — ``@allow_sync`` only
|
|
143
|
+
unwraps one awaitable, and buffering a stream would defeat streaming.
|
|
144
|
+
"""
|
|
145
|
+
body = self._build_body(
|
|
146
|
+
messages,
|
|
147
|
+
stream=stream,
|
|
148
|
+
tools=tools,
|
|
149
|
+
model=model,
|
|
150
|
+
internal=internal,
|
|
151
|
+
**kwargs,
|
|
152
|
+
)
|
|
153
|
+
if stream:
|
|
154
|
+
return self._complete_stream(body)
|
|
155
|
+
return self._complete(body)
|
|
156
|
+
|
|
157
|
+
@allow_sync
|
|
158
|
+
async def _complete(self, body: dict[str, Any]) -> dict[str, Any]:
|
|
159
|
+
self.info(
|
|
160
|
+
f"[ChatCompletionsClient] Completing {body['model']!r} — "
|
|
161
|
+
f"messages={len(body['messages'])}, tools={'tools' in body}"
|
|
162
|
+
)
|
|
163
|
+
response = await self.post("chat/completions", json=body)
|
|
164
|
+
response.raise_for_status()
|
|
165
|
+
if not response.data:
|
|
166
|
+
raise ValueError(f"chat/completions returned non-JSON body: {response.text!r}")
|
|
167
|
+
return dict(response.data)
|
|
168
|
+
|
|
169
|
+
async def _complete_stream(self, body: dict[str, Any]) -> AsyncIterator[dict[str, Any]]:
|
|
170
|
+
self.info(
|
|
171
|
+
f"[ChatCompletionsClient] Streaming {body['model']!r} — "
|
|
172
|
+
f"messages={len(body['messages'])}, tools={'tools' in body}"
|
|
173
|
+
)
|
|
174
|
+
async with self.stream_request(HTTPMethod.POST, "chat/completions", json=body) as response:
|
|
175
|
+
if int(response.status_code) >= 400:
|
|
176
|
+
error_body = (await response.aread()).decode("utf-8", errors="replace")
|
|
177
|
+
BaseApiResponse(status_code=response.status_code, text=error_body).raise_for_status()
|
|
178
|
+
async for chunk in self._stream_decoder.decode(response):
|
|
179
|
+
yield chunk
|