From 9854c4eaa201bc26fbfd2899fff44242539e9a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Yusuf=20Sar=C4=B1g=C3=B6z?= Date: Sat, 1 Aug 2026 08:29:35 +0300 Subject: [PATCH 1/4] make: update google-genai --- DESIGN.md | 4 +- afterimage/__init__.py | 1 + afterimage/logging.py | 24 ++++++ afterimage/providers/llm_providers.py | 82 ++++++++++++++++--- afterimage/simula/cli_logging.py | 16 +--- pyproject.toml | 2 +- tests/test_gemini_provider.py | 113 ++++++++++++++++++++++++++ uv.lock | 25 ++++-- 8 files changed, 228 insertions(+), 39 deletions(-) create mode 100644 afterimage/logging.py create mode 100644 tests/test_gemini_provider.py diff --git a/DESIGN.md b/DESIGN.md index cdb07e2..a4ff400 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -11,7 +11,7 @@ The library is designed around a few core concepts: - **SamplingStrategy**: Encapsulates persona selection, context/document sampling, and instruction dispatch logic. Infers target usage counts from stopping callbacks and configures sampling for both context and persona pipelines. - **QualityGate**: Wraps the evaluator (ConversationJudge) and retry logic for the `auto_improve` workflow. Determines whether a generated conversation meets quality thresholds or needs regeneration. - **PersonaGenerator**: Analyzes documents to generate diverse user personas, enhancing dataset variety. -- **LLMProvider**: An abstraction over different language model providers (Gemini, OpenAI compatible). +- **LLMProvider**: An abstraction over different language model providers (Gemini, OpenAI compatible). The `google-genai` package dependency is upgraded (`>=1.50.0`) with built-in support for Gemini thought signatures in multi-turn conversations and automatic reasoning extraction (`reasoning_content`) from candidate thought parts. - **EmbeddingProvider**: Async-first text embeddings (`async def embed(texts) -> list[list[float]]`). API backends (`OpenAIEmbeddingProvider`, `GeminiEmbeddingProvider`) use each vendor’s async client and `SmartKeyPool`; `ProcessEmbeddingProvider` runs SentenceTransformer in a `ProcessPoolExecutor` so the asyncio loop is not blocked by local inference (install the `embeddings-local` extra for `sentence-transformers`). Use `EmbeddingProviderFactory.create({...})` in `afterimage/providers/embedding_providers.py`. - **DatasetStorage**: An abstraction for storing and loading generated conversations and documents. It supports JSONL and SQL backends. - **Callbacks**: These allow for customization of the generation process. @@ -61,7 +61,7 @@ The code is organized into the following directories and files: - `retrievers.py`: Context retrieval strategies for RAG (`ContextRetriever`, `RetrievalResult`, optional `*_context_with_metadata`, `QdrantRetriever`, `StaticContextRetriever`, composite retrievers). - `storage.py`: Storage backends (JSONL, SQL). - `types.py`: Data models using Pydantic. - - `simula/`: **OpenSimula (experimental)** — `OpenSimula` orchestrator, `taxonomy_builder` (optional `show_progress` + tqdm), `cli_logging` (`configure_example_console`, `silence_noisy_third_party_loggers`), `sampling`, `meta_prompt`, `critics`, `double_critic`, `evaluation`, `document_context`, and `tasks/` (single QA, MCQ, multiturn instruction callback). + - `simula/`: **OpenSimula (experimental)** — `OpenSimula` orchestrator, `taxonomy_builder` (optional `show_progress` + tqdm), `cli_logging` (`configure_example_console`, `silence_noisy_third_party_loggers` with default `ERROR` level to mute `google_genai` API key warnings and HTTP noise), `sampling`, `meta_prompt`, `critics`, `double_critic`, `evaluation`, `document_context`, and `tasks/` (single QA, MCQ, multiturn instruction callback). - `evaluation/`: The new evaluation framework. - `__init__.py`: Exposes evaluation classes. - `base.py`: Base classes for evaluators. diff --git a/afterimage/__init__.py b/afterimage/__init__.py index bb75a9f..ec2f385 100644 --- a/afterimage/__init__.py +++ b/afterimage/__init__.py @@ -27,6 +27,7 @@ default_embedding_provider_config, # noqa ) from afterimage.key_management import SmartKeyPool # noqa +from afterimage.logging import silence_noisy_third_party_loggers # noqa from afterimage.orchestrator import Orchestrator # noqa from afterimage.quality_gate import QualityGate, QualityResult # noqa from afterimage.sampling import SamplingStrategy # noqa diff --git a/afterimage/logging.py b/afterimage/logging.py new file mode 100644 index 0000000..c13528b --- /dev/null +++ b/afterimage/logging.py @@ -0,0 +1,24 @@ +"""Central logging utilities for AfterImage.""" + +from __future__ import annotations + +import logging + +_NOISY_LOGGERS = ( + "httpx", + "httpcore", + "httpcore.connection", + "httpcore.http11", + "google_genai", + "google_genai._api_client", + "google_genai.client", + "google_genai.models", + "google.auth", + "google.auth.transport", +) + + +def silence_noisy_third_party_loggers(level: int = logging.ERROR) -> None: + """Turn down chatty HTTP and google-genai log lines (warnings/info).""" + for name in _NOISY_LOGGERS: + logging.getLogger(name).setLevel(level) diff --git a/afterimage/providers/llm_providers.py b/afterimage/providers/llm_providers.py index 89fbcc9..7476c75 100644 --- a/afterimage/providers/llm_providers.py +++ b/afterimage/providers/llm_providers.py @@ -131,6 +131,46 @@ def _gemini_retry_delay( return random.uniform(base * 0.5, base * 1.5) +def _extract_gemini_text_and_reasoning(response: Any) -> tuple[str, str | None]: + """Safely extract main output text and reasoning content from a Gemini response.""" + try: + candidates = getattr(response, "candidates", None) + if not candidates: + return getattr(response, "text", "") or "", None + + candidate = candidates[0] + content = getattr(candidate, "content", None) + if not content: + return getattr(response, "text", "") or "", None + + parts = getattr(content, "parts", None) + if not parts: + return getattr(response, "text", "") or "", None + + text_pieces: list[str] = [] + thought_pieces: list[str] = [] + + for part in parts: + is_thought = getattr(part, "thought", False) + part_text = getattr(part, "text", None) + if part_text: + if is_thought: + thought_pieces.append(part_text) + else: + text_pieces.append(part_text) + + main_text = ( + "".join(text_pieces) + if text_pieces + else (getattr(response, "text", "") or "") + ) + reasoning = "\n".join(thought_pieces).strip() if thought_pieces else None + return main_text, reasoning + except Exception: + return getattr(response, "text", "") or "", None + + + class GeminiChatSession(ChatSession): """Gemini chat session implementation.""" @@ -172,14 +212,16 @@ def send_message( raise time.sleep(self._retry_delay(attempt)) + text, reasoning = _extract_gemini_text_and_reasoning(response) return LLMResponse( - text=response.text, + text=text, finish_reason=str(response.candidates[0].finish_reason), prompt_token_count=response.usage_metadata.prompt_token_count, completion_token_count=response.usage_metadata.candidates_token_count, total_token_count=response.usage_metadata.total_token_count, model_name=self.model_name, raw_response=response, + reasoning_content=reasoning, ) def close(self) -> None: @@ -230,16 +272,18 @@ async def asend_message( raise await asyncio.sleep(self._retry_delay(attempt)) + text, reasoning = _extract_gemini_text_and_reasoning(response) total_token_count = response.usage_metadata.total_token_count self.token_count = total_token_count return LLMResponse( - text=response.text, + text=text, finish_reason=str(response.candidates[0].finish_reason), prompt_token_count=response.usage_metadata.prompt_token_count, completion_token_count=response.usage_metadata.candidates_token_count, total_token_count=total_token_count, model_name=self.model_name, raw_response=response, + reasoning_content=reasoning, ) async def aclose(self) -> None: @@ -535,14 +579,16 @@ def generate_content( config=generation_config, ) + text, reasoning = _extract_gemini_text_and_reasoning(response) return LLMResponse( - text=response.text, + text=text, prompt_token_count=response.usage_metadata.prompt_token_count, completion_token_count=response.usage_metadata.candidates_token_count, total_token_count=response.usage_metadata.total_token_count, finish_reason=str(response.candidates[0].finish_reason), model_name=self.model_name, raw_response=response, + reasoning_content=reasoning, ) except Exception as exc: @@ -584,14 +630,16 @@ async def agenerate_content( config=generation_config, ) + text, reasoning = _extract_gemini_text_and_reasoning(response) return LLMResponse( - text=response.text, + text=text, prompt_token_count=response.usage_metadata.prompt_token_count, completion_token_count=response.usage_metadata.candidates_token_count, total_token_count=response.usage_metadata.total_token_count, finish_reason=str(response.candidates[0].finish_reason), model_name=self.model_name, raw_response=response, + reasoning_content=reasoning, ) except Exception as exc: @@ -630,17 +678,22 @@ def generate_structured( config=generation_config, ) + text, reasoning = _extract_gemini_text_and_reasoning(response) + parsed_val = ( + response.parsed + if hasattr(response, "parsed") and response.parsed is not None + else schema.model_validate_json(text) + ) return StructuredLLMResponse( - text=response.text or "", - parsed=response.parsed - if hasattr(response, "parsed") - else schema.model_validate_json(response.text), + text=text, + parsed=parsed_val, prompt_token_count=response.usage_metadata.prompt_token_count, completion_token_count=response.usage_metadata.candidates_token_count, total_token_count=response.usage_metadata.total_token_count, finish_reason=str(response.candidates[0].finish_reason), model_name=self.model_name, raw_response=response, + reasoning_content=reasoning, ) except Exception as exc: @@ -678,17 +731,22 @@ async def agenerate_structured( contents=prompt, config=generation_config, ) + text, reasoning = _extract_gemini_text_and_reasoning(response) + parsed_val = ( + response.parsed + if hasattr(response, "parsed") and response.parsed is not None + else schema.model_validate_json(text) + ) return StructuredLLMResponse( - text=response.text or "", - parsed=response.parsed - if hasattr(response, "parsed") - else schema.model_validate_json(response.text), + text=text, + parsed=parsed_val, prompt_token_count=response.usage_metadata.prompt_token_count, completion_token_count=response.usage_metadata.candidates_token_count, total_token_count=response.usage_metadata.total_token_count, finish_reason=str(response.candidates[0].finish_reason), model_name=self.model_name, raw_response=response, + reasoning_content=reasoning, ) except Exception as exc: diff --git a/afterimage/simula/cli_logging.py b/afterimage/simula/cli_logging.py index cade35d..760debf 100644 --- a/afterimage/simula/cli_logging.py +++ b/afterimage/simula/cli_logging.py @@ -4,23 +4,9 @@ import logging -_NOISY_LOGGERS = ( - "httpx", - "httpcore", - "httpcore.connection", - "httpcore.http11", - "google_genai", - "google_genai.models", - "google.auth", - "google.auth.transport", -) +from ..logging import silence_noisy_third_party_loggers -def silence_noisy_third_party_loggers(level: int = logging.WARNING) -> None: - """Turn down chatty HTTP and google-genai log lines during OpenSimula runs.""" - for name in _NOISY_LOGGERS: - logging.getLogger(name).setLevel(level) - def configure_example_console( *, diff --git a/pyproject.toml b/pyproject.toml index f1ee60a..7278547 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "click>=8.0.0", "faker>=20.0.0", "filelock", - "google-genai==1.38.0", + "google-genai>=1.50.0", "grpcio==1.67.1", "huggingface-hub>=0.22.0", "langdetect>=1.0.9", diff --git a/tests/test_gemini_provider.py b/tests/test_gemini_provider.py new file mode 100644 index 0000000..ef2120d --- /dev/null +++ b/tests/test_gemini_provider.py @@ -0,0 +1,113 @@ +import pytest +from unittest.mock import MagicMock, AsyncMock +from pydantic import BaseModel + +from afterimage.providers.llm_providers import ( + GeminiProvider, + GeminiChatSession, + AsyncGeminiChatSession, + _extract_gemini_text_and_reasoning, +) + + +class SampleSchema(BaseModel): + answer: str + + +def test_extract_gemini_text_and_reasoning_standard(): + response = MagicMock() + part = MagicMock() + part.thought = False + part.text = "Hello world" + candidate = MagicMock() + candidate.content.parts = [part] + response.candidates = [candidate] + + text, reasoning = _extract_gemini_text_and_reasoning(response) + assert text == "Hello world" + assert reasoning is None + + +def test_extract_gemini_text_and_reasoning_with_thought(): + response = MagicMock() + thought_part = MagicMock() + thought_part.thought = True + thought_part.text = "Let me think step by step..." + + text_part = MagicMock() + text_part.thought = False + text_part.text = "Here is the final answer." + + candidate = MagicMock() + candidate.content.parts = [thought_part, text_part] + response.candidates = [candidate] + + text, reasoning = _extract_gemini_text_and_reasoning(response) + assert text == "Here is the final answer." + assert reasoning == "Let me think step by step..." + + +def test_extract_gemini_text_and_reasoning_fallback(): + response = MagicMock() + response.candidates = None + response.text = "Fallback text" + + text, reasoning = _extract_gemini_text_and_reasoning(response) + assert text == "Fallback text" + assert reasoning is None + + +def test_gemini_chat_session_send_message(): + mock_chat = MagicMock() + mock_response = MagicMock() + mock_response.text = "Chat answer" + mock_response.candidates = [MagicMock()] + mock_response.candidates[0].finish_reason = "STOP" + mock_response.usage_metadata.prompt_token_count = 10 + mock_response.usage_metadata.candidates_token_count = 5 + mock_response.usage_metadata.total_token_count = 15 + mock_chat.send_message.return_value = mock_response + + mock_client = MagicMock() + session = GeminiChatSession( + chat=mock_chat, + client=mock_client, + model_name="gemini-2.0-flash", + ) + + resp = session.send_message("Hello") + assert resp.text == "Chat answer" + assert resp.total_token_count == 15 + mock_chat.send_message.assert_called_once_with("Hello") + + +@pytest.mark.asyncio +async def test_async_gemini_chat_session_asend_message(): + mock_chat = MagicMock() + mock_response = MagicMock() + mock_response.text = "Async answer" + mock_response.candidates = [MagicMock()] + mock_response.candidates[0].finish_reason = "STOP" + mock_response.usage_metadata.prompt_token_count = 20 + mock_response.usage_metadata.candidates_token_count = 10 + mock_response.usage_metadata.total_token_count = 30 + mock_chat.send_message = AsyncMock(return_value=mock_response) + + mock_client = MagicMock() + session = AsyncGeminiChatSession( + chat=mock_chat, + client=mock_client, + model_name="gemini-2.0-flash", + ) + + resp = await session.asend_message("Hi async") + assert resp.text == "Async answer" + assert resp.total_token_count == 30 + mock_chat.send_message.assert_called_once_with("Hi async") + + +def test_gemini_provider_init(): + provider = GeminiProvider(api_key="test_key", model_name="gemini-2.0-flash") + assert provider.model_name == "gemini-2.0-flash" + assert provider.key_pool is not None + diff --git a/uv.lock b/uv.lock index 7ec0b19..648b4c1 100644 --- a/uv.lock +++ b/uv.lock @@ -36,7 +36,7 @@ wheels = [ [[package]] name = "afterimage" -version = "0.17.0" +version = "0.18.0" source = { editable = "." } dependencies = [ { name = "click" }, @@ -107,7 +107,7 @@ requires-dist = [ { name = "fastapi", marker = "extra == 'server'", specifier = ">=0.115.0" }, { name = "fastmcp", marker = "extra == 'training'", specifier = ">=3.2.0" }, { name = "filelock" }, - { name = "google-genai", specifier = "==1.38.0" }, + { name = "google-genai", specifier = ">=1.50.0" }, { name = "gradio", marker = "extra == 'training'", specifier = ">=6.3.0" }, { name = "grpcio", specifier = "==1.67.1" }, { name = "huggingface-hub", specifier = ">=0.22.0" }, @@ -1306,34 +1306,41 @@ http = [ [[package]] name = "google-auth" -version = "2.49.1" +version = "2.56.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/33/dbc946a407401b975f0719658f18e664ece2109f79ffd1ff3bf226c205f4/google_auth-2.56.2.tar.gz", hash = "sha256:e28f103ca8091fb7012b99c44243d7366c29863713b8e34a220c3322b7a07051", size = 365820, upload-time = "2026-07-21T21:53:28.188Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, + { url = "https://files.pythonhosted.org/packages/88/63/50636aae68c9bf17c891c7eb18b49baa9bd6b31d2a97b8de4813a9fc8d1c/google_auth-2.56.2-py3-none-any.whl", hash = "sha256:c8270ea95b2697b74e3d8438ae9c5b898e38b623b915c7b5c5635921e7de68a6", size = 258588, upload-time = "2026-07-21T21:53:26.399Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, ] [[package]] name = "google-genai" -version = "1.38.0" +version = "2.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "google-auth" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, { name = "httpx" }, { name = "pydantic" }, { name = "requests" }, + { name = "sniffio" }, { name = "tenacity" }, { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/11/108ddd3aca8af6a9e2369e59b9646a3a4c64aefb39d154f6467ab8d79f34/google_genai-1.38.0.tar.gz", hash = "sha256:363272fc4f677d0be6a1aed7ebabe8adf45e1626a7011a7886a587e9464ca9ec", size = 244903, upload-time = "2025-09-16T23:25:42.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e6/ff83088427072cc9d5d21036788cf0ed08cc4906e4a5810e469553a43185/google_genai-2.16.0.tar.gz", hash = "sha256:c4c2524926001b18073db927a5d75bb7c8be7b5fd13ab507d599f51fff2284c5", size = 647939, upload-time = "2026-07-30T14:34:37.366Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/6c/1de711bab3c118284904c3bedf870519e8c63a7a8e0905ac3833f1db9cbc/google_genai-1.38.0-py3-none-any.whl", hash = "sha256:95407425132d42b3fa11bc92b3f5cf61a0fbd8d9add1f0e89aac52c46fbba090", size = 245558, upload-time = "2025-09-16T23:25:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f111056110030b1a5fb949687d7f93c2b4e8996f6494ae32efb049482796/google_genai-2.16.0-py3-none-any.whl", hash = "sha256:f9eda6a7a3dd4491a0d2253c4bdd4536462d63838ed3f1b0e4fb9a0eb8f43331", size = 1050096, upload-time = "2026-07-30T14:34:35.578Z" }, ] [[package]] From 36d650f948485bcec66fdbab9c24233e87a7646d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Yusuf=20Sar=C4=B1g=C3=B6z?= Date: Sat, 1 Aug 2026 08:35:25 +0300 Subject: [PATCH 2/4] make: suppress noisy third party loggers more globally --- DESIGN.md | 1 + afterimage/cli.py | 3 +++ afterimage/providers/llm_providers.py | 2 ++ examples/agent_trace_generator_demo.py | 2 +- tests/test_logging.py | 23 +++++++++++++++++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 tests/test_logging.py diff --git a/DESIGN.md b/DESIGN.md index a4ff400..107405c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -54,6 +54,7 @@ The code is organized into the following directories and files: It also holds provider-aware concurrency defaults. - `evaluator.py`: `ConversationJudge` and embedding defaults for auto-improve. - `key_management.py`: Smart API key management with rate limiting. + - `logging.py`: Central logging utilities (`silence_noisy_third_party_loggers` with default `ERROR` level to suppress `google_genai` API key warnings and HTTP noise). - `monitoring.py`: Monitoring system implementation. - `persona_generator.py`: **[NEW]** Logic for generating personas from documents. - `prompts.py`: Default prompts (instruction generation, respondent persona etc.). diff --git a/afterimage/cli.py b/afterimage/cli.py index 72b487b..8922367 100644 --- a/afterimage/cli.py +++ b/afterimage/cli.py @@ -22,6 +22,9 @@ @click.version_option(package_name="afterimage") def main(): """AfterImage -- synthetic conversation dataset generator.""" + from .logging import silence_noisy_third_party_loggers + + silence_noisy_third_party_loggers() @main.command("agent-trace") diff --git a/afterimage/providers/llm_providers.py b/afterimage/providers/llm_providers.py index 7476c75..45320b4 100644 --- a/afterimage/providers/llm_providers.py +++ b/afterimage/providers/llm_providers.py @@ -12,6 +12,7 @@ from ..common import default_safety_settings from ..key_management import SmartKeyPool +from ..logging import silence_noisy_third_party_loggers from ..types import MODEL_PROVIDER_NAMES, ConversationEntry, ModelProviderName T = TypeVar("T", bound=BaseModel) @@ -540,6 +541,7 @@ def __init__( self.retry_initial_delay = retry_initial_delay self.retry_max_delay = retry_max_delay self.kwargs = kwargs + silence_noisy_third_party_loggers() def _retry_delay(self, attempt: int) -> float: return _gemini_retry_delay( diff --git a/examples/agent_trace_generator_demo.py b/examples/agent_trace_generator_demo.py index 91a011d..217d32a 100644 --- a/examples/agent_trace_generator_demo.py +++ b/examples/agent_trace_generator_demo.py @@ -98,7 +98,7 @@ async def main(): teacher_model="gemini-3.5-flash-lite", judge_model="gemini-3.6-flash", observation_mode="llm", # Preferred production mode (ESAT paper LLM observation synthesis). - task_synthesis_mode="simula", + task_synthesis_mode="grid", context_generator=VirtualUserContextGenerator(seed=42), ) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..726caca --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,23 @@ +import logging +import os +from afterimage.logging import silence_noisy_third_party_loggers + + +def test_silence_noisy_third_party_loggers(): + silence_noisy_third_party_loggers(logging.ERROR) + assert logging.getLogger("google_genai").level == logging.ERROR + assert logging.getLogger("httpx").level == logging.ERROR + + +def test_google_genai_warning_suppressed(capsys): + os.environ["GOOGLE_API_KEY"] = "test_google_key" + os.environ["GEMINI_API_KEY"] = "test_gemini_key" + + silence_noisy_third_party_loggers(logging.ERROR) + + from google import genai + _ = genai.Client() + + captured = capsys.readouterr() + assert "Both GOOGLE_API_KEY and GEMINI_API_KEY are set" not in captured.err + assert "Both GOOGLE_API_KEY and GEMINI_API_KEY are set" not in captured.out From f37014d33c5a6dee1afed921eb5894aa4643b584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Yusuf=20Sar=C4=B1g=C3=B6z?= Date: Sat, 1 Aug 2026 08:36:00 +0300 Subject: [PATCH 3/4] make: bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7278547..f4a37e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "afterimage" -version = "0.18.0" +version = "0.18.1" keywords = ["afterimage", "dataset-generation", "synthetic-dataset", "tool-calling", "structured-output", "preference-data", "persona-driven"] authors = [ { name = "Yusuf Sarıgöz", email = "yusuf@altai.dev"} From 8ac1e0d2f16095d3252a07e1d87ccf7aba46d367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2E=20Yusuf=20Sar=C4=B1g=C3=B6z?= Date: Sat, 1 Aug 2026 08:36:36 +0300 Subject: [PATCH 4/4] chor: apply formatting --- afterimage/providers/llm_providers.py | 1 - afterimage/simula/cli_logging.py | 1 - tests/test_gemini_provider.py | 1 - tests/test_logging.py | 1 + 4 files changed, 1 insertion(+), 3 deletions(-) diff --git a/afterimage/providers/llm_providers.py b/afterimage/providers/llm_providers.py index 45320b4..079f9a8 100644 --- a/afterimage/providers/llm_providers.py +++ b/afterimage/providers/llm_providers.py @@ -171,7 +171,6 @@ def _extract_gemini_text_and_reasoning(response: Any) -> tuple[str, str | None]: return getattr(response, "text", "") or "", None - class GeminiChatSession(ChatSession): """Gemini chat session implementation.""" diff --git a/afterimage/simula/cli_logging.py b/afterimage/simula/cli_logging.py index 760debf..1c982f4 100644 --- a/afterimage/simula/cli_logging.py +++ b/afterimage/simula/cli_logging.py @@ -7,7 +7,6 @@ from ..logging import silence_noisy_third_party_loggers - def configure_example_console( *, simula_level: int = logging.WARNING, diff --git a/tests/test_gemini_provider.py b/tests/test_gemini_provider.py index ef2120d..98436a7 100644 --- a/tests/test_gemini_provider.py +++ b/tests/test_gemini_provider.py @@ -110,4 +110,3 @@ def test_gemini_provider_init(): provider = GeminiProvider(api_key="test_key", model_name="gemini-2.0-flash") assert provider.model_name == "gemini-2.0-flash" assert provider.key_pool is not None - diff --git a/tests/test_logging.py b/tests/test_logging.py index 726caca..f2da016 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -16,6 +16,7 @@ def test_google_genai_warning_suppressed(capsys): silence_noisy_third_party_loggers(logging.ERROR) from google import genai + _ = genai.Client() captured = capsys.readouterr()