Files
privateGPT/tests/test_di.py
Javier Martinez cd8ca2214a feat: resumable chat worker + tool worker + async tokenizer (#2298)
* fix: avoid to block the loop

* fix: blocks in expansion

* fix: remove maximum concurrent users

...

* fix: multiplexer

* fix: readers

* fix: more fixes

...

* fix: impl

* feat: tool scheduler

* feat: add adaptative

* feat: add chat worker

* fix: max

* feat: add chat/tools workers

* fix: mypy

* feat: add generic scheduler

* fix: get result

* feat: do serializable the tool executor

* fix: tools

* fix: config

* fix: config

* fix: args

* fix: config

* fix: serializer

* Revert "fix: blocks in expansion"

This reverts commit a2110f94a8.

* fix: unify all logic

* feat: add ingestion scheduler

* fix: settings

* fix: config

* feat: add arq worker to chat

* fix: arq worker

* fix: add nest

* fix: mypy

* fix: await

* fix: script stress

* fix: tokenizer

* fix: chat scheduler

* fix: mypy

* fix: add async tokenizer

* fix: improve condense

* fix: tool scheduler

* feat: add initial real async chat worker

* fix: mypy

* fix: do resumable local executor

...

...

...

fix: revert usleess changes

fix: remove parent chat job

fix: refactor

fix: loop

ref: rename models

fix: chat engine

fix: mypy

...

...

...

fix: fix deps

* fix: tests

* fix: tests

* ...

* fix: stream

* fix: config

* fix: scheduler

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Handle PGPT_WORKER_MODE=celery in health check worker status

* fix: cancel

* fix: arch

* fix: test ingestion

* fix: deserialization of chat messages

* fix: broken results

* fix: mypy

* fix: test

* fix: config

* fix: remove arq tool worker

* fix: output cls

* fix: preserve early resumable tool callbacks

* fix: preserve async tool result order

* refactor: address worker PR review comments

* fix: mypy

* test: colocate ARQ chat enqueue coverage

* fix: remove redis from tests

* test: isolate chat mocks and cancellation timing

* fix: tests

(cherry picked from commit 218b599c66)

# Conflicts:
#	tests/server/chat/anthropic/test_anthropic_client.py
#	tests/server/chat/anthropic/test_langchain_anthropic.py
#	tests/server/chat/test_chat_knowledge_revamp.py
#	tests/server/chat/test_chat_routes.py
#	tests/server/chat/test_chat_routes_skills_integration.py

* fix: tests

(cherry picked from commit fc5ec0f72a)

* fix: ruff

* fix: test

* fix: worker config

(cherry picked from commit 1371c275a1)

* fix: principal

* test: remove flaky chat cancellation assertion

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-07-15 12:12:45 +02:00

169 lines
5.4 KiB
Python

import asyncio
import threading
from typing import Any
from private_gpt.components.node_store.node_store_component import NodeStoreComponent
from private_gpt.components.vector_store.vector_store_component import (
VectorStoreComponent,
)
from private_gpt.di import (
clean_global_injector,
get_global_injector,
get_injector,
)
def test_injector_is_shared_across_loops_when_global_root_exists() -> None:
class IdentifiableService:
instance_count = 0
def __init__(self) -> None:
self.id: int = id(self)
self.instance_number: int = IdentifiableService.instance_count
IdentifiableService.instance_count += 1
self.created_in_thread: int = threading.get_ident()
async def operation(self, should_fail: bool = False) -> dict[str, Any]:
if should_fail:
raise ValueError("Operation failed intentionally")
return {
"service_id": self.id,
"instance_number": self.instance_number,
"thread_id": threading.get_ident(),
}
rollback_called: bool = False
error_value: Exception | None = None
def rollback_fn(error: Exception) -> None:
nonlocal rollback_called, error_value
rollback_called = True
error_value = error
def run_task_in_thread(task_id: int, should_fail: bool = False) -> dict[str, Any]:
result: dict[str, Any] = {
"task_id": task_id,
"thread_id": None,
"service_id": None,
"instance_number": None,
"error": None,
"success": False,
}
thread_injector = None
def thread_func() -> None:
nonlocal thread_injector
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
async def async_task() -> None:
nonlocal thread_injector
injector = get_injector()
thread_injector = injector
service = IdentifiableService()
injector.binder.bind(IdentifiableService, to=service)
try:
retrieved_service = injector.get(IdentifiableService)
result["service_id"] = retrieved_service.id
result["instance_number"] = retrieved_service.instance_number
result["thread_id"] = threading.get_ident()
service_result = await retrieved_service.operation(should_fail)
result.update(service_result)
result["success"] = True
except Exception as e:
result["error"] = e
rollback_fn(e)
loop.run_until_complete(async_task())
finally:
loop.close()
thread = threading.Thread(target=thread_func)
thread.start()
thread.join(timeout=5.0)
result["injector"] = thread_injector
return result
results: list[dict[str, Any]] = []
for i in range(3):
should_fail = i % 3 == 2
result = run_task_in_thread(i, should_fail)
results.append(result)
injectors = [r["injector"] for r in results]
injector_ids = [id(inj) for inj in injectors]
assert len(set(injector_ids)) == 1
service_ids = [r["service_id"] for r in results]
assert len(set(service_ids)) == len(results)
assert rollback_called
assert isinstance(error_value, ValueError)
for i, result in enumerate(results):
should_fail = i % 3 == 2
if should_fail:
assert result["error"] is not None
assert not result["success"]
else:
assert result["error"] is None
assert result["success"]
print("\nDetailed test results:")
for i, r in enumerate(results):
print(f"\nTask {i}:")
print(f" Thread ID: {r['thread_id']}")
print(f" Injector ID: {id(r['injector'])}")
print(f" Service ID: {r['service_id']}")
print(f" Service Instance #: {r['instance_number']}")
print(f" Success: {r['success']}")
if r["error"]:
print(f" Error: {r['error']}")
def test_global_fallback_injector() -> None:
class MarkerService:
def __init__(self) -> None:
self.id: int = id(self)
global_injector = get_global_injector()
marker = MarkerService()
global_injector.binder.bind(MarkerService, to=marker)
retrieved_injector = get_global_injector()
retrieved_marker = retrieved_injector.get(MarkerService)
assert id(global_injector) == id(retrieved_injector)
assert retrieved_marker.id == marker.id
standard_injector = get_injector()
assert id(standard_injector) == id(global_injector)
def test_clean_global_injector() -> None:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
async def run_in_loop() -> None:
old_global_injector = get_global_injector()
assert old_global_injector is not None
old_global_injector.get(VectorStoreComponent)
old_global_injector.get(NodeStoreComponent)
running_loop = asyncio.get_running_loop()
await clean_global_injector(running_loop)
new_injector = get_global_injector()
assert new_injector is not None
assert new_injector == old_global_injector
loop.run_until_complete(run_in_loop())