pi-sidecar-client 0.1.0.dev2__tar.gz → 0.1.0.dev4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pi-sidecar-client
3
- Version: 0.1.0.dev2
3
+ Version: 0.1.0.dev4
4
4
  Summary: Python client for the Pi SDK sidecar service
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/myk-org/pi-sidecar
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import inspect
3
3
  import os
4
+ import tempfile
4
5
  from collections.abc import Callable
5
6
  from dataclasses import dataclass
6
7
  from typing import Any
@@ -11,6 +12,7 @@ from simple_logger.logger import get_logger
11
12
  logger = get_logger(name=__name__, level=os.environ.get("LOG_LEVEL", "INFO"))
12
13
 
13
14
  SIDECAR_URL = os.environ.get("SIDECAR_URL", "http://127.0.0.1:9100")
15
+ DEFAULT_CWD = tempfile.gettempdir()
14
16
 
15
17
  __all__ = [
16
18
  "AIResult",
@@ -48,7 +50,8 @@ _usage_recorder: Callable | None = None
48
50
  def set_usage_recorder(callback: Callable) -> None:
49
51
  """Register a callback for recording AI token usage.
50
52
 
51
- The callback signature:
53
+ The callback may be sync or async:
54
+ def recorder(*, request_id, result, call_type, prompt_chars, ai_provider, ai_model)
52
55
  async def recorder(*, request_id, result, call_type, prompt_chars, ai_provider, ai_model)
53
56
  """
54
57
  global _usage_recorder
@@ -115,6 +118,7 @@ class SidecarClient:
115
118
  def __init__(self, base_url: str = SIDECAR_URL):
116
119
  self._base_url = base_url.rstrip("/")
117
120
  self._client = httpx.AsyncClient(base_url=self._base_url, timeout=600.0)
121
+ self._closed = False
118
122
 
119
123
  async def health(self) -> dict:
120
124
  """Check sidecar health."""
@@ -140,7 +144,7 @@ class SidecarClient:
140
144
  provider: str,
141
145
  model: str,
142
146
  system_prompt: str,
143
- cwd: str = "/tmp",
147
+ cwd: str = DEFAULT_CWD,
144
148
  custom_tools: list | None = None,
145
149
  ) -> str:
146
150
  """Create a new AI session. Returns session_id."""
@@ -202,6 +206,7 @@ class SidecarClient:
202
206
  async def close(self) -> None:
203
207
  """Close the HTTP client."""
204
208
  await self._client.aclose()
209
+ self._closed = True
205
210
 
206
211
 
207
212
  # Singleton client
@@ -211,7 +216,7 @@ _client: SidecarClient | None = None
211
216
  def get_sidecar_client() -> SidecarClient:
212
217
  """Get the singleton sidecar client."""
213
218
  global _client
214
- if _client is None:
219
+ if _client is None or _client._closed:
215
220
  _client = SidecarClient()
216
221
  return _client
217
222
 
@@ -250,7 +255,7 @@ async def call_ai(
250
255
  provider=ai_provider,
251
256
  model=ai_model,
252
257
  system_prompt=system_prompt or "You are a helpful assistant.",
253
- cwd=cwd or "/tmp",
258
+ cwd=cwd or DEFAULT_CWD,
254
259
  custom_tools=custom_tools,
255
260
  )
256
261
  created_session = True
@@ -289,9 +294,10 @@ async def call_ai_once(
289
294
  ) -> AIResult:
290
295
  """Single-shot AI call with automatic session cleanup.
291
296
 
292
- Creates a session, sends the prompt, and always deletes the session.
293
- Use this for one-off calls (analysis, bug creation, etc.).
294
- Use ``call_ai`` directly for multi-turn conversations (peer debate).
297
+ Creates a session, sends the prompt, and deletes the session.
298
+ Cleanup is best-effort if deletion fails, result.session_id is
299
+ preserved so the caller can retry cleanup.
300
+ Use ``call_ai`` directly for multi-turn conversations.
295
301
  """
296
302
  result = await call_ai(
297
303
  prompt,
@@ -327,13 +333,20 @@ async def check_sidecar_available() -> tuple[bool, str]:
327
333
  """Check if the sidecar service is available and ready."""
328
334
  try:
329
335
  client = get_sidecar_client()
330
- resp = await client._client.get("/health")
331
- data = resp.json()
332
- if resp.status_code == 200 and data.get("status") == "ok":
336
+ data = await client.health()
337
+ if data.get("status") == "ok":
333
338
  return True, "Sidecar is ready"
334
- if resp.status_code == 503 and data.get("status") == "starting":
339
+ if data.get("status") == "starting":
335
340
  return False, f"Sidecar starting: {data.get('message', 'model discovery in progress')}"
336
- return False, f"Sidecar unhealthy (HTTP {resp.status_code}): {data}"
341
+ return False, f"Sidecar unhealthy: {data}"
342
+ except httpx.HTTPStatusError as e:
343
+ if e.response.status_code == 503:
344
+ try:
345
+ data = e.response.json()
346
+ return False, f"Sidecar starting: {data.get('message', 'model discovery in progress')}"
347
+ except ValueError:
348
+ pass
349
+ return False, f"Sidecar unhealthy (HTTP {e.response.status_code})"
337
350
  except Exception as e:
338
351
  return False, f"Sidecar unavailable: {e}"
339
352
 
@@ -343,6 +356,9 @@ async def run_parallel_with_limit(
343
356
  max_concurrency: int = 5,
344
357
  ) -> list:
345
358
  """Run async tasks in parallel with concurrency limit."""
359
+ if max_concurrency < 1:
360
+ raise ValueError("max_concurrency must be >= 1")
361
+
346
362
  semaphore = asyncio.Semaphore(max_concurrency)
347
363
 
348
364
  async def limited(coro):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pi-sidecar-client
3
- Version: 0.1.0.dev2
3
+ Version: 0.1.0.dev4
4
4
  Summary: Python client for the Pi SDK sidecar service
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/myk-org/pi-sidecar
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pi-sidecar-client"
7
- version = "0.1.0.dev2"
7
+ version = "0.1.0.dev4"
8
8
  description = "Python client for the Pi SDK sidecar service"
9
9
  license = "Apache-2.0"
10
10
  requires-python = ">=3.10"