agstack 1.0.8__tar.gz → 1.2.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.
Files changed (50) hide show
  1. {agstack-1.0.8 → agstack-1.2.0}/PKG-INFO +5 -5
  2. {agstack-1.0.8 → agstack-1.2.0}/agstack/config/logger.py +8 -4
  3. agstack-1.2.0/agstack/contexts.py +21 -0
  4. {agstack-1.0.8 → agstack-1.2.0}/agstack/decorators.py +9 -4
  5. {agstack-1.0.8 → agstack-1.2.0}/agstack/events.py +4 -4
  6. {agstack-1.0.8 → agstack-1.2.0}/agstack/fastapi/middleware.py +8 -2
  7. {agstack-1.0.8 → agstack-1.2.0}/agstack/infra/db/__init__.py +2 -2
  8. {agstack-1.0.8 → agstack-1.2.0}/agstack/infra/es/__init__.py +2 -2
  9. {agstack-1.0.8 → agstack-1.2.0}/agstack/infra/kg/__init__.py +4 -3
  10. {agstack-1.0.8 → agstack-1.2.0}/agstack/infra/mq/__init__.py +1 -1
  11. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/client.py +17 -5
  12. {agstack-1.0.8 → agstack-1.2.0}/agstack.egg-info/PKG-INFO +5 -5
  13. {agstack-1.0.8 → agstack-1.2.0}/agstack.egg-info/SOURCES.txt +1 -0
  14. {agstack-1.0.8 → agstack-1.2.0}/agstack.egg-info/requires.txt +4 -4
  15. {agstack-1.0.8 → agstack-1.2.0}/pyproject.toml +7 -6
  16. {agstack-1.0.8 → agstack-1.2.0}/LICENSE +0 -0
  17. {agstack-1.0.8 → agstack-1.2.0}/README.md +0 -0
  18. {agstack-1.0.8 → agstack-1.2.0}/agstack/__init__.py +0 -0
  19. {agstack-1.0.8 → agstack-1.2.0}/agstack/config/__init__.py +0 -0
  20. {agstack-1.0.8 → agstack-1.2.0}/agstack/config/manager.py +0 -0
  21. {agstack-1.0.8 → agstack-1.2.0}/agstack/config/types.py +0 -0
  22. {agstack-1.0.8 → agstack-1.2.0}/agstack/exceptions.py +0 -0
  23. {agstack-1.0.8 → agstack-1.2.0}/agstack/fastapi/__init__.py +0 -0
  24. {agstack-1.0.8 → agstack-1.2.0}/agstack/fastapi/exception.py +0 -0
  25. {agstack-1.0.8 → agstack-1.2.0}/agstack/fastapi/offline.py +0 -0
  26. {agstack-1.0.8 → agstack-1.2.0}/agstack/fastapi/sse.py +0 -0
  27. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/__init__.py +0 -0
  28. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/__init__.py +0 -0
  29. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/agent.py +0 -0
  30. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/context.py +0 -0
  31. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/events.py +0 -0
  32. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/exceptions.py +0 -0
  33. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/factory.py +0 -0
  34. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/flow.py +0 -0
  35. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/loader.py +0 -0
  36. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/records.py +0 -0
  37. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/registry.py +0 -0
  38. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/state.py +0 -0
  39. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/flow/tool.py +0 -0
  40. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/prompts.py +0 -0
  41. {agstack-1.0.8 → agstack-1.2.0}/agstack/llm/token.py +0 -0
  42. {agstack-1.0.8 → agstack-1.2.0}/agstack/registry.py +0 -0
  43. {agstack-1.0.8 → agstack-1.2.0}/agstack/schema.py +0 -0
  44. {agstack-1.0.8 → agstack-1.2.0}/agstack/security/__init__.py +0 -0
  45. {agstack-1.0.8 → agstack-1.2.0}/agstack/security/casbin.py +0 -0
  46. {agstack-1.0.8 → agstack-1.2.0}/agstack/security/crypt.py +0 -0
  47. {agstack-1.0.8 → agstack-1.2.0}/agstack/status.py +0 -0
  48. {agstack-1.0.8 → agstack-1.2.0}/agstack.egg-info/dependency_links.txt +0 -0
  49. {agstack-1.0.8 → agstack-1.2.0}/agstack.egg-info/top_level.txt +0 -0
  50. {agstack-1.0.8 → agstack-1.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agstack
3
- Version: 1.0.8
3
+ Version: 1.2.0
4
4
  Summary: Production-ready toolkit for building FastAPI and LLM applications
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -24,18 +24,18 @@ Requires-Dist: ag-ui-protocol>=0.1.13
24
24
  Requires-Dist: aio-pika>=9.6.1
25
25
  Requires-Dist: asyncpg>=0.30.0
26
26
  Requires-Dist: elasticsearch[async]>=9.3.0
27
- Requires-Dist: fastapi>=0.133.0
27
+ Requires-Dist: fastapi>=0.133.1
28
28
  Requires-Dist: jwcrypto>=1.5.6
29
29
  Requires-Dist: loguru>=0.7.3
30
30
  Requires-Dist: nebula3-python>=3.8.3
31
- Requires-Dist: openai>=2.24.0
31
+ Requires-Dist: openai>=2.26.0
32
32
  Requires-Dist: passlib[bcrypt]>=1.7.4
33
33
  Requires-Dist: pycasbin>=2.8.0
34
34
  Requires-Dist: pydantic>=2.12.4
35
35
  Requires-Dist: python-multipart>=0.0.20
36
36
  Requires-Dist: requests>=2.32.5
37
- Requires-Dist: sqlalchemy[asyncio]>=2.0.47
38
- Requires-Dist: sqlobjects>=1.2.3
37
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.48
38
+ Requires-Dist: sqlobjects>=1.2.5
39
39
  Requires-Dist: tiktoken>=0.12.0
40
40
  Requires-Dist: uvicorn>=0.41.0
41
41
  Dynamic: license-file
@@ -26,16 +26,20 @@ class InterceptHandler(logging.Handler):
26
26
  level = record.levelno
27
27
 
28
28
  # 直接使用 logging.LogRecord 的信息,避免栈帧查找
29
- logger.patch(
29
+ patched = logger.patch(
30
30
  lambda r: r.update(
31
31
  name=record.name,
32
- file={"name": record.pathname, "path": record.pathname}, # type: ignore[arg-type]
32
+ file={"name": record.pathname, "path": record.pathname}, # type: ignore
33
33
  line=record.lineno,
34
34
  function=record.funcName,
35
35
  )
36
- ).log(level, record.getMessage())
36
+ )
37
+
38
+ if record.exc_info:
39
+ patched.opt(exception=record.exc_info).log(level, record.getMessage())
40
+ else:
41
+ patched.log(level, record.getMessage())
37
42
 
38
- # CRITICAL/FATAL 级别自动退出
39
43
  if record.levelno >= logging.CRITICAL:
40
44
  sys.exit(1)
41
45
 
@@ -0,0 +1,21 @@
1
+ # Copyright (c) 2020-2025 XtraVisions, All rights reserved.
2
+
3
+ """运行时上下文"""
4
+
5
+ import uuid
6
+ from contextvars import ContextVar, Token
7
+
8
+
9
+ _request_id: ContextVar[str | None] = ContextVar("request_id", default=None)
10
+
11
+
12
+ def get_request_id() -> str:
13
+ return _request_id.get() or str(uuid.uuid4())
14
+
15
+
16
+ def set_request_id(value: str) -> Token:
17
+ return _request_id.set(value)
18
+
19
+
20
+ def reset_request_id(token: Token) -> None:
21
+ _request_id.reset(token)
@@ -4,6 +4,7 @@ import asyncio
4
4
  import time
5
5
  from functools import wraps
6
6
  from logging import Logger
7
+ from typing import Awaitable, Callable, ParamSpec, TypeVar
7
8
 
8
9
  from sqlobjects.session import ctx_session, has_session
9
10
 
@@ -49,17 +50,21 @@ def autoretry(
49
50
  return decorator
50
51
 
51
52
 
52
- def with_session(func, dbname: str | None = None):
53
+ P = ParamSpec("P")
54
+ R = TypeVar("R")
55
+
56
+
57
+ def with_session(func: Callable[P, Awaitable[R]], dbname: str | None = None) -> Callable[P, Awaitable[R]]:
53
58
  """装饰器:确保方法在 session 上下文中执行,支持嵌套调用"""
54
59
 
55
60
  @wraps(func)
56
- async def wrapper(self, *args, **kwargs):
61
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
57
62
  # 如果已经在 session 上下文中,直接执行
58
63
  if has_session(dbname):
59
- return await func(self, *args, **kwargs)
64
+ return await func(*args, **kwargs)
60
65
 
61
66
  # 否则创建新的 session 上下文
62
67
  async with ctx_session(dbname):
63
- return await func(self, *args, **kwargs)
68
+ return await func(*args, **kwargs)
64
69
 
65
70
  return wrapper
@@ -13,10 +13,10 @@ class EventType(str, Enum):
13
13
  """事件类型定义"""
14
14
 
15
15
  # Infra 生命周期
16
- DB_CONNECTED = "db.connected"
17
- ES_CONNECTED = "es.connected"
18
- MQ_CONNECTED = "mq.connected"
19
- KG_CONNECTED = "kg.connected"
16
+ DB_INITED = "db.inited"
17
+ ES_INITED = "es.inited"
18
+ MQ_INITED = "mq.inited"
19
+ KG_INITED = "kg.inited"
20
20
 
21
21
  # 组件生命周期
22
22
  COMPONENT_REGISTERED = "component.registered"
@@ -8,6 +8,8 @@ from typing import Callable
8
8
  from fastapi import Request, Response
9
9
  from starlette.middleware.base import BaseHTTPMiddleware
10
10
 
11
+ from ..contexts import reset_request_id, set_request_id
12
+
11
13
 
12
14
  class RequestIDMiddleware(BaseHTTPMiddleware):
13
15
  """请求 ID 中间件
@@ -22,8 +24,12 @@ class RequestIDMiddleware(BaseHTTPMiddleware):
22
24
  # 将 request_id 存储到 request.state(供后续使用)
23
25
  request.state.request_id = request_id
24
26
 
25
- # 调用下一个处理器
26
- response = await call_next(request)
27
+ token = set_request_id(request_id)
28
+
29
+ try:
30
+ response = await call_next(request)
31
+ finally:
32
+ reset_request_id(token)
27
33
 
28
34
  # 在响应头中返回 request_id
29
35
  response.headers["X-Request-ID"] = request_id
@@ -27,7 +27,7 @@ async def setup_db(
27
27
  **engine_kwargs: Any,
28
28
  ):
29
29
  url = f"postgresql+asyncpg://{username}:{password}@{host}/{database}"
30
- await init_db(
30
+ db = await init_db(
31
31
  url,
32
32
  echo=echo,
33
33
  pool_size=pool_size,
@@ -36,7 +36,7 @@ async def setup_db(
36
36
  pool_recycle=pool_recycle,
37
37
  **engine_kwargs,
38
38
  )
39
- await event_bus.publish(EventType.DB_CONNECTED)
39
+ await event_bus.publish(EventType.DB_INITED, {"db": db})
40
40
 
41
41
 
42
42
  async def shutdown_db():
@@ -14,7 +14,7 @@ async def setup_es(
14
14
  sniff_timeout: float = 3,
15
15
  ):
16
16
  # 为 elasticsearch-dsl 创建连接
17
- async_connections.connections.create_connection(
17
+ es = async_connections.connections.create_connection(
18
18
  alias="default",
19
19
  hosts=hosts,
20
20
  verify_certs=False,
@@ -24,7 +24,7 @@ async def setup_es(
24
24
  sniff_on_node_failure=True,
25
25
  http_auth=(username, password),
26
26
  )
27
- await event_bus.publish(EventType.ES_CONNECTED)
27
+ await event_bus.publish(EventType.ES_INITED, {"es": es})
28
28
 
29
29
 
30
30
  async def shutdown_es():
@@ -42,17 +42,18 @@ async def setup_kg(
42
42
  if not pool.init(hosts, config):
43
43
  raise RuntimeError("Failed to initialize NebulaGraph connection pool")
44
44
 
45
- # 验证连接
45
+ # 验证连接,space 不存在时自动创建
46
46
  session = pool.get_session(username, password)
47
47
  try:
48
- result = session.execute(f"USE {space}")
48
+ session.execute(f"CREATE SPACE IF NOT EXISTS `{space}` (vid_type=FIXED_STRING(64))")
49
+ result = session.execute(f"USE `{space}`")
49
50
  if not result.is_succeeded():
50
51
  raise RuntimeError(f"Failed to use space '{space}': {result.error_msg()}")
51
52
  finally:
52
53
  session.release()
53
54
 
54
55
  _context = _KGContext(pool=pool, space=space, username=username, password=password)
55
- await event_bus.publish(EventType.KG_CONNECTED)
56
+ await event_bus.publish(EventType.KG_INITED, {"pool": pool})
56
57
 
57
58
 
58
59
  async def shutdown_kg():
@@ -20,7 +20,7 @@ async def setup_mq(host: str, port: int, username: str, password: str):
20
20
  connection_url = f"amqp://{username}:{password}@{host}:{port}/"
21
21
  _connection = await aio_pika.connect_robust(connection_url, loop=get_event_loop())
22
22
 
23
- await event_bus.publish(EventType.MQ_CONNECTED)
23
+ await event_bus.publish(EventType.MQ_INITED, {"connection": _connection})
24
24
 
25
25
 
26
26
  async def shutdown_mq():
@@ -5,9 +5,12 @@ import time
5
5
  from typing import TYPE_CHECKING, Any, AsyncIterator, Literal, overload
6
6
 
7
7
  from httpx import AsyncClient
8
+ from httpx import Timeout as HttpxTimeout
8
9
  from openai import APIError, APITimeoutError, AsyncOpenAI, OpenAI, RateLimitError
10
+ from openai import Timeout as OpenAITimeout
9
11
  from openai.types.chat import ChatCompletionMessageParam
10
12
 
13
+ from ..contexts import get_request_id
11
14
  from ..exceptions import AppException
12
15
 
13
16
 
@@ -51,16 +54,20 @@ class LLMClient:
51
54
  """
52
55
  self._base_url = base_url
53
56
 
57
+ # 只限制连接超时,读取不限(推理时间不可预估)
58
+ _openai_timeout = OpenAITimeout(connect=10.0, read=None, write=None, pool=None)
59
+ _httpx_timeout = HttpxTimeout(connect=10.0, read=None, write=None, pool=None)
60
+
54
61
  # 异步客户端
55
62
  self._async_client = AsyncOpenAI(
56
63
  base_url=base_url,
57
64
  api_key=api_key,
58
- timeout=60.0,
65
+ timeout=_openai_timeout,
59
66
  max_retries=0, # 手动控制重试
60
67
  )
61
68
  self._async_http_client = AsyncClient(
62
69
  headers={"Authorization": f"Bearer {api_key}"},
63
- timeout=60.0,
70
+ timeout=_httpx_timeout,
64
71
  )
65
72
 
66
73
  # 同步客户端(延迟初始化)
@@ -69,7 +76,7 @@ class LLMClient:
69
76
  self._sync_client_config = {
70
77
  "base_url": base_url,
71
78
  "api_key": api_key,
72
- "timeout": 60.0,
79
+ "timeout": _openai_timeout,
73
80
  "max_retries": 0,
74
81
  }
75
82
 
@@ -150,6 +157,7 @@ class LLMClient:
150
157
  messages=messages,
151
158
  temperature=temperature,
152
159
  max_tokens=max_tokens,
160
+ extra_headers={"X-Request-ID": get_request_id()},
153
161
  **kwargs,
154
162
  )
155
163
 
@@ -255,6 +263,7 @@ class LLMClient:
255
263
  max_tokens=max_tokens,
256
264
  stream=True,
257
265
  stream_options={"include_usage": True},
266
+ extra_headers={"X-Request-ID": get_request_id()},
258
267
  **kwargs,
259
268
  )
260
269
 
@@ -293,7 +302,9 @@ class LLMClient:
293
302
  exceptions=(APITimeoutError, ConnectionError),
294
303
  )
295
304
  async def _call():
296
- return await self._async_client.embeddings.create(model=model, input=texts)
305
+ return await self._async_client.embeddings.create(
306
+ model=model, input=texts, extra_headers={"X-Request-ID": get_request_id()}
307
+ )
297
308
 
298
309
  try:
299
310
  response = await _call()
@@ -447,6 +458,7 @@ class LLMClient:
447
458
  "top_n": top_n,
448
459
  "return_documents": True,
449
460
  },
461
+ headers={"X-Request-ID": get_request_id()},
450
462
  )
451
463
 
452
464
  try:
@@ -495,7 +507,7 @@ class LLMClient:
495
507
  "top_n": top_n,
496
508
  "return_documents": True,
497
509
  },
498
- timeout=60.0,
510
+ timeout=None,
499
511
  )
500
512
  response.raise_for_status()
501
513
  data = response.json()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agstack
3
- Version: 1.0.8
3
+ Version: 1.2.0
4
4
  Summary: Production-ready toolkit for building FastAPI and LLM applications
5
5
  Author-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
6
6
  Maintainer-email: XtraVisions <gitadmin@xtravisions.com>, Chen Hao <chenhao@xtravisions.com>
@@ -24,18 +24,18 @@ Requires-Dist: ag-ui-protocol>=0.1.13
24
24
  Requires-Dist: aio-pika>=9.6.1
25
25
  Requires-Dist: asyncpg>=0.30.0
26
26
  Requires-Dist: elasticsearch[async]>=9.3.0
27
- Requires-Dist: fastapi>=0.133.0
27
+ Requires-Dist: fastapi>=0.133.1
28
28
  Requires-Dist: jwcrypto>=1.5.6
29
29
  Requires-Dist: loguru>=0.7.3
30
30
  Requires-Dist: nebula3-python>=3.8.3
31
- Requires-Dist: openai>=2.24.0
31
+ Requires-Dist: openai>=2.26.0
32
32
  Requires-Dist: passlib[bcrypt]>=1.7.4
33
33
  Requires-Dist: pycasbin>=2.8.0
34
34
  Requires-Dist: pydantic>=2.12.4
35
35
  Requires-Dist: python-multipart>=0.0.20
36
36
  Requires-Dist: requests>=2.32.5
37
- Requires-Dist: sqlalchemy[asyncio]>=2.0.47
38
- Requires-Dist: sqlobjects>=1.2.3
37
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.48
38
+ Requires-Dist: sqlobjects>=1.2.5
39
39
  Requires-Dist: tiktoken>=0.12.0
40
40
  Requires-Dist: uvicorn>=0.41.0
41
41
  Dynamic: license-file
@@ -2,6 +2,7 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  agstack/__init__.py
5
+ agstack/contexts.py
5
6
  agstack/decorators.py
6
7
  agstack/events.py
7
8
  agstack/exceptions.py
@@ -2,17 +2,17 @@ ag-ui-protocol>=0.1.13
2
2
  aio-pika>=9.6.1
3
3
  asyncpg>=0.30.0
4
4
  elasticsearch[async]>=9.3.0
5
- fastapi>=0.133.0
5
+ fastapi>=0.133.1
6
6
  jwcrypto>=1.5.6
7
7
  loguru>=0.7.3
8
8
  nebula3-python>=3.8.3
9
- openai>=2.24.0
9
+ openai>=2.26.0
10
10
  passlib[bcrypt]>=1.7.4
11
11
  pycasbin>=2.8.0
12
12
  pydantic>=2.12.4
13
13
  python-multipart>=0.0.20
14
14
  requests>=2.32.5
15
- sqlalchemy[asyncio]>=2.0.47
16
- sqlobjects>=1.2.3
15
+ sqlalchemy[asyncio]>=2.0.48
16
+ sqlobjects>=1.2.5
17
17
  tiktoken>=0.12.0
18
18
  uvicorn>=0.41.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agstack"
3
- version = "1.0.8"
3
+ version = "1.2.0"
4
4
  description = "Production-ready toolkit for building FastAPI and LLM applications"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -43,18 +43,18 @@ dependencies = [
43
43
  "aio-pika>=9.6.1",
44
44
  "asyncpg>=0.30.0",
45
45
  "elasticsearch[async]>=9.3.0",
46
- "fastapi>=0.133.0",
46
+ "fastapi>=0.133.1",
47
47
  "jwcrypto>=1.5.6",
48
48
  "loguru>=0.7.3",
49
49
  "nebula3-python>=3.8.3",
50
- "openai>=2.24.0",
50
+ "openai>=2.26.0",
51
51
  "passlib[bcrypt]>=1.7.4",
52
52
  "pycasbin>=2.8.0",
53
53
  "pydantic>=2.12.4",
54
54
  "python-multipart>=0.0.20",
55
55
  "requests>=2.32.5",
56
- "sqlalchemy[asyncio]>=2.0.47",
57
- "sqlobjects>=1.2.3",
56
+ "sqlalchemy[asyncio]>=2.0.48",
57
+ "sqlobjects>=1.2.5",
58
58
  "tiktoken>=0.12.0",
59
59
  "uvicorn>=0.41.0",
60
60
  ]
@@ -62,7 +62,7 @@ dependencies = [
62
62
  dev = [
63
63
  "pre-commit>=4.4.0",
64
64
  "pyright>=1.1.407",
65
- "ruff>=0.15.2",
65
+ "ruff>=0.15.5",
66
66
  "setuptools>=82.0.0",
67
67
  ]
68
68
 
@@ -96,6 +96,7 @@ select = [
96
96
  ]
97
97
  ignore = [
98
98
  "UP046", # disable none pep695 generic class
99
+ "UP047", # disable none pep695 generic function
99
100
  "UP035", # disable using `type` instead `types.Type`
100
101
  ]
101
102
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes