Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
- uses: ./.github/actions/setup-python-uv
with:
python-version: ${{ matrix.python-version }}
- run: uv run pytest tests/ -v --cov=celeste --cov-report=term-missing --cov-report=xml --cov-report=html --cov-fail-under=90
- run: uv run pytest tests/unit_tests -v --cov=celeste --cov-report=term-missing --cov-report=xml --cov-report=html --cov-fail-under=90
- uses: codecov/codecov-action@v4
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
with:
Expand Down
19 changes: 18 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,26 @@ jobs:
uses: ./.github/workflows/ci.yml
secrets: inherit

build:
integration-test:
needs: [validate-release, run-ci]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: ./.github/actions/setup-python-uv
- name: Run integration tests
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}
run: make integration-test

build:
needs: [validate-release, run-ci, integration-test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ repos:
hooks:
- id: pytest
name: "🧪 Run tests with coverage"
entry: uv run pytest tests/ --cov=celeste --cov-report=term-missing
entry: uv run pytest tests/unit_tests --cov=celeste --cov-report=term-missing
language: system
types: [python]
pass_filenames: false
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help sync lint lint-fix format typecheck test security ci clean
.PHONY: help sync lint lint-fix format typecheck test integration-test security ci clean

# Default target
help:
Expand All @@ -8,6 +8,7 @@ help:
@echo " make format - Apply Ruff formatting"
@echo " make typecheck - Run mypy type checking"
@echo " make test - Run pytest with coverage"
@echo " make integration-test - Run integration tests (requires API keys)"
@echo " make security - Run Bandit security scan"
@echo " make ci - Run full CI/CD pipeline"
@echo " make clean - Clean cache directories"
Expand Down Expand Up @@ -36,7 +37,11 @@ typecheck:

# Testing
test:
uv run pytest tests/ --cov=celeste --cov-report=term-missing --cov-fail-under=90
uv run pytest tests/unit_tests --cov=celeste --cov-report=term-missing --cov-fail-under=90

# Integration testing (requires API keys)
integration-test:
uv run pytest tests/integration_tests/ -m integration -v --dist=worksteal -n auto

# Security scanning (config reads from pyproject.toml)
security:
Expand Down
81 changes: 81 additions & 0 deletions packages/text-generation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<div align="center">

# <img src="../../logo.svg" width="48" height="48" alt="Celeste Logo" style="vertical-align: middle;"> Celeste Text Generation

**Text Generation capability for Celeste AI**

[![Python](https://img.shields.io/badge/Python-3.12+-blue?style=for-the-badge)](https://www.python.org/)
[![License](https://img.shields.io/badge/License-Apache_2.0-red?style=for-the-badge)](../../LICENSE)

[Quick Start](#-quick-start) • [Documentation](https://withceleste.ai/docs) • [Request Provider](https://github.com/withceleste/celeste-python/issues/new)

</div>

---

## 🚀 Quick Start

```python
from celeste import create_client, Capability, Provider

client = create_client(
capability=Capability.TEXT_GENERATION,
provider=Provider.OPENAI,
)

response = await client.generate(prompt="Hello, world!")
print(response.content)
```

**Install:**
```bash
uv add "celeste-ai[text-generation]"
```

---

## Supported Providers


<div align="center">

<img src="https://www.google.com/s2/favicons?domain=google.com&sz=64" width="64" height="64" alt="Google" title="Google">
<img src="https://www.google.com/s2/favicons?domain=anthropic.com&sz=64" width="64" height="64" alt="Anthropic" title="Anthropic">
<img src="https://www.google.com/s2/favicons?domain=openai.com&sz=64" width="64" height="64" alt="OpenAI" title="OpenAI">
<img src="https://mistral.ai/favicon.ico" width="64" height="64" alt="Mistral" title="Mistral">
<img src="https://www.google.com/s2/favicons?domain=cohere.com&sz=64" width="64" height="64" alt="Cohere" title="Cohere">


**Missing a provider?** [Request it](https://github.com/withceleste/celeste-python/issues/new) – ⚡ **we ship fast**.

</div>

---

**Streaming**: ✅ Supported

**Parameters**: See [API Documentation](https://withceleste.ai/docs/api) for full parameter reference.

---

## 🤝 Contributing

See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines.

**Request a provider:** [GitHub Issues](https://github.com/withceleste/celeste-python/issues/new)

---

## 📄 License

Apache 2.0 License – see [LICENSE](../../LICENSE) for details.

---

<div align="center">

**[Get Started](https://withceleste.ai/docs/quickstart)** • **[Documentation](https://withceleste.ai/docs)** • **[GitHub](https://github.com/withceleste/celeste-python)**

Made with ❤️ by developers tired of framework lock-in

</div>
39 changes: 39 additions & 0 deletions packages/text-generation/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[project]
name = "text-generation"
version = "0.0.3"
description = "Type-safe text generation for Celeste AI. Unified interface for OpenAI, Anthropic, Google, Mistral, Cohere, and more"
authors = [{name = "Kamilbenkirane", email = "kamil@withceleste.ai"}]
readme = "README.md"
license = {text = "Apache-2.0"}
requires-python = ">=3.12"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Operating System :: OS Independent",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Typing :: Typed",
]
keywords = ["ai", "text-generation", "llm", "openai", "anthropic", "claude", "gemini", "mistral", "cohere"]

[project.urls]
Homepage = "https://withceleste.ai"
Documentation = "https://withceleste.ai/docs"
Repository = "https://github.com/withceleste/celeste-python"
Issues = "https://github.com/withceleste/celeste-python/issues"

[tool.uv.sources]
celeste-ai = { workspace = true }

[project.entry-points."celeste.packages"]
text_generation = "celeste_text_generation:register_package"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/celeste_text_generation"]
33 changes: 33 additions & 0 deletions packages/text-generation/src/celeste_text_generation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Celeste text generation capability."""


def register_package() -> None:
"""Register text generation package (client and models)."""
from celeste.client import register_client
from celeste.core import Capability
from celeste.models import register_models
from celeste_text_generation.models import MODELS
from celeste_text_generation.providers import PROVIDERS

# Register provider-specific clients
for provider, client_class in PROVIDERS:
register_client(Capability.TEXT_GENERATION, provider, client_class)

register_models(MODELS, capability=Capability.TEXT_GENERATION)


# Import after register_package is defined to avoid circular imports
from celeste_text_generation.io import ( # noqa: E402
TextGenerationInput,
TextGenerationOutput,
TextGenerationUsage,
)
from celeste_text_generation.streaming import TextGenerationStream # noqa: E402

__all__ = [
"TextGenerationInput",
"TextGenerationOutput",
"TextGenerationStream",
"TextGenerationUsage",
"register_package",
]
82 changes: 82 additions & 0 deletions packages/text-generation/src/celeste_text_generation/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Base client for text generation."""

from abc import abstractmethod
from typing import Any, Unpack

import httpx
from pydantic import BaseModel

from celeste.client import Client
from celeste_text_generation.io import (
TextGenerationFinishReason,
TextGenerationInput,
TextGenerationOutput,
TextGenerationUsage,
)
from celeste_text_generation.parameters import TextGenerationParameters


class TextGenerationClient(
Client[TextGenerationInput, TextGenerationOutput, TextGenerationParameters]
):
"""Client for text generation operations."""

@abstractmethod
def _init_request(self, inputs: TextGenerationInput) -> dict[str, Any]:
"""Initialize provider-specific request structure."""
...

@abstractmethod
def _parse_usage(self, response_data: dict[str, Any]) -> TextGenerationUsage:
"""Parse usage information from provider response."""
...

@abstractmethod
def _parse_content(
self,
response_data: dict[str, Any],
**parameters: Unpack[TextGenerationParameters],
) -> str | BaseModel:
"""Parse content from provider response."""
...

@abstractmethod
def _parse_finish_reason(
self, response_data: dict[str, Any]
) -> TextGenerationFinishReason | None:
"""Parse finish reason from provider response."""
...

def _create_inputs(
self, *args: str, **parameters: Unpack[TextGenerationParameters]
) -> TextGenerationInput:
"""Map positional arguments to Input type."""
if args:
return TextGenerationInput(prompt=args[0])
prompt = parameters.get("prompt")
if prompt is None:
msg = (
"prompt is required (either as positional argument or keyword argument)"
)
raise TypeError(msg)
return TextGenerationInput(prompt=prompt)

@classmethod
def _output_class(cls) -> type[TextGenerationOutput]:
"""Return the Output class for this client."""
return TextGenerationOutput

def _build_metadata(self, response_data: dict[str, Any]) -> dict[str, Any]:
"""Build metadata dictionary from response data."""
metadata = super()._build_metadata(response_data)
metadata["finish_reason"] = self._parse_finish_reason(response_data)
return metadata

@abstractmethod
async def _make_request(
self,
request_body: dict[str, Any],
**parameters: Unpack[TextGenerationParameters],
) -> httpx.Response:
"""Make HTTP request(s) and return response object."""
...
57 changes: 57 additions & 0 deletions packages/text-generation/src/celeste_text_generation/io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Input and output types for text generation."""

from celeste.io import Chunk, FinishReason, Input, Output, Usage


class TextGenerationInput(Input):
"""Input for text generation requests."""

prompt: str


class TextGenerationFinishReason(FinishReason):
"""Text generation finish reason.

Stores raw provider reason. Providers map their values in implementation.
"""

reason: str # Raw provider string (e.g., "stop", "end_turn", "STOP", "COMPLETE")


class TextGenerationUsage(Usage):
"""Text generation usage metrics.

All fields optional since providers vary.
"""

input_tokens: int | None = None
output_tokens: int | None = None
total_tokens: int | None = None
billed_tokens: int | None = None
cached_tokens: int | None = None
reasoning_tokens: int | None = None


class TextGenerationOutput[Content](Output[Content]):
"""Output with text or structured content."""

pass


class TextGenerationChunk(Chunk[str]):
"""Typed chunk for text generation streaming.

Content is incremental text delta.
"""

finish_reason: TextGenerationFinishReason | None = None
usage: TextGenerationUsage | None = None


__all__ = [
"TextGenerationChunk",
"TextGenerationFinishReason",
"TextGenerationInput",
"TextGenerationOutput",
"TextGenerationUsage",
]
18 changes: 18 additions & 0 deletions packages/text-generation/src/celeste_text_generation/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Model definitions for text generation."""

from celeste import Model
from celeste_text_generation.providers.anthropic.models import (
MODELS as ANTHROPIC_MODELS,
)
from celeste_text_generation.providers.cohere.models import MODELS as COHERE_MODELS
from celeste_text_generation.providers.google.models import MODELS as GOOGLE_MODELS
from celeste_text_generation.providers.mistral.models import MODELS as MISTRAL_MODELS
from celeste_text_generation.providers.openai.models import MODELS as OPENAI_MODELS

MODELS: list[Model] = [
*ANTHROPIC_MODELS,
*COHERE_MODELS,
*GOOGLE_MODELS,
*MISTRAL_MODELS,
*OPENAI_MODELS,
]
Loading
Loading