dptyan-core 0.1.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.
- dptyan_core-0.1.0/PKG-INFO +31 -0
- dptyan_core-0.1.0/README.md +0 -0
- dptyan_core-0.1.0/pyproject.toml +55 -0
- dptyan_core-0.1.0/src/dptyan_core/__init__.py +0 -0
- dptyan_core-0.1.0/src/dptyan_core/core/aiclient/base.py +16 -0
- dptyan_core-0.1.0/src/dptyan_core/core/aiclient/openai.py +36 -0
- dptyan_core-0.1.0/src/dptyan_core/core/base/const.py +38 -0
- dptyan_core-0.1.0/src/dptyan_core/core/base/struct.py +67 -0
- dptyan_core-0.1.0/src/dptyan_core/core/embeddingclient/base.py +20 -0
- dptyan_core-0.1.0/src/dptyan_core/core/embeddingclient/openai.py +47 -0
- dptyan_core-0.1.0/src/dptyan_core/core/embeddingclient/transformers.py +35 -0
- dptyan_core-0.1.0/src/dptyan_core/core/module/base.py +26 -0
- dptyan_core-0.1.0/src/dptyan_core/py.typed +0 -0
- dptyan_core-0.1.0/src/dptyan_core/server/api/client.py +27 -0
- dptyan_core-0.1.0/src/dptyan_core/server/depends.py +15 -0
- dptyan_core-0.1.0/src/dptyan_core/server/lifespan.py +153 -0
- dptyan_core-0.1.0/src/dptyan_core/server/main.py +55 -0
- dptyan_core-0.1.0/src/dptyan_core/server/models/event.py +9 -0
- dptyan_core-0.1.0/src/dptyan_core/server/models/runtime.py +85 -0
- dptyan_core-0.1.0/src/dptyan_core/server/routes/event.py +27 -0
- dptyan_core-0.1.0/src/dptyan_core/server/tasks/executor.py +57 -0
- dptyan_core-0.1.0/src/dptyan_core/server/tasks/worker.py +191 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dptyan-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Core for autonomus 'DPTyan' AI.
|
|
5
|
+
Author: nesquikcode
|
|
6
|
+
Author-email: nesquikcode <miharu@qnya.ru>
|
|
7
|
+
Requires-Dist: datetime>=6.0
|
|
8
|
+
Requires-Dist: langchain-core>=1.6.1
|
|
9
|
+
Requires-Dist: pydantic>=2.13.4
|
|
10
|
+
Requires-Dist: aiohttp>=3.14.3 ; extra == 'async-client'
|
|
11
|
+
Requires-Dist: dptyan-core[openaiclient] ; extra == 'full'
|
|
12
|
+
Requires-Dist: dptyan-core[server] ; extra == 'full'
|
|
13
|
+
Requires-Dist: dptyan-core[local-embeddings] ; extra == 'full'
|
|
14
|
+
Requires-Dist: dptyan-core[async-client] ; extra == 'full'
|
|
15
|
+
Requires-Dist: sentence-transformers>=3.1.1 ; extra == 'local-embeddings'
|
|
16
|
+
Requires-Dist: openai>=3.5.0 ; extra == 'openaiclient'
|
|
17
|
+
Requires-Dist: fastapi>=0.141.1 ; extra == 'server'
|
|
18
|
+
Requires-Dist: langchain-qdrant>=1.1.0 ; extra == 'server'
|
|
19
|
+
Requires-Dist: qdrant-client>=1.19.0 ; extra == 'server'
|
|
20
|
+
Requires-Dist: tokenizers==0.15.2 ; extra == 'server'
|
|
21
|
+
Requires-Dist: torch>=2.11.0 ; extra == 'server'
|
|
22
|
+
Requires-Dist: torchvision>=0.26.0 ; extra == 'server'
|
|
23
|
+
Requires-Dist: uvicorn>=0.52.4 ; extra == 'server'
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Provides-Extra: async-client
|
|
26
|
+
Provides-Extra: full
|
|
27
|
+
Provides-Extra: local-embeddings
|
|
28
|
+
Provides-Extra: openaiclient
|
|
29
|
+
Provides-Extra: server
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
File without changes
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "dptyan-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Core for autonomus 'DPTyan' AI."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "nesquikcode", email = "miharu@qnya.ru" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"datetime>=6.0",
|
|
12
|
+
"langchain-core>=1.6.1",
|
|
13
|
+
"pydantic>=2.13.4",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.11.24,<0.12.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
openaiclient = [
|
|
22
|
+
"openai>=3.5.0"
|
|
23
|
+
]
|
|
24
|
+
server = [
|
|
25
|
+
"fastapi>=0.141.1",
|
|
26
|
+
"langchain-qdrant>=1.1.0",
|
|
27
|
+
"qdrant-client>=1.19.0",
|
|
28
|
+
"tokenizers==0.15.2",
|
|
29
|
+
"torch>=2.11.0",
|
|
30
|
+
"torchvision>=0.26.0",
|
|
31
|
+
"uvicorn>=0.52.4",
|
|
32
|
+
]
|
|
33
|
+
full = [
|
|
34
|
+
"dptyan-core[openaiclient]",
|
|
35
|
+
"dptyan-core[server]",
|
|
36
|
+
"dptyan-core[local-embeddings]",
|
|
37
|
+
"dptyan-core[async-client]"
|
|
38
|
+
]
|
|
39
|
+
local-embeddings = [
|
|
40
|
+
"sentence-transformers>=3.1.1",
|
|
41
|
+
]
|
|
42
|
+
async-client = [
|
|
43
|
+
"aiohttp>=3.14.3",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
[[tool.uv.index]]
|
|
48
|
+
name = "pytorch"
|
|
49
|
+
url = "https://download.pytorch.org/whl/cu128"
|
|
50
|
+
explicit = true
|
|
51
|
+
|
|
52
|
+
[tool.uv.sources]
|
|
53
|
+
ram = { git = "https://github.com/xinyu1205/recognize-anything.git" }
|
|
54
|
+
torch = { index = "pytorch" }
|
|
55
|
+
torchvision = { index = "pytorch" }
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
class AIClient(ABC):
|
|
4
|
+
|
|
5
|
+
def __init__(self, model: str, system_prompt: str, **params):
|
|
6
|
+
self.model = model
|
|
7
|
+
self.system_prompt = system_prompt
|
|
8
|
+
self.default_params = params
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
async def generate(
|
|
12
|
+
self,
|
|
13
|
+
text: str,
|
|
14
|
+
system_prompt: str | None = None,
|
|
15
|
+
**params
|
|
16
|
+
) -> str:...
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dptyan_core.core.aiclient.base import AIClient
|
|
2
|
+
|
|
3
|
+
from openai import AsyncOpenAI
|
|
4
|
+
|
|
5
|
+
class OpenAIClient(AIClient):
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
model: str,
|
|
10
|
+
system_prompt: str,
|
|
11
|
+
openai_token: str,
|
|
12
|
+
base_url: str | None = None,
|
|
13
|
+
**params
|
|
14
|
+
):
|
|
15
|
+
super().__init__(model, system_prompt, **params)
|
|
16
|
+
self._oaitoken = openai_token
|
|
17
|
+
self._baseurl = base_url
|
|
18
|
+
self._client = AsyncOpenAI(
|
|
19
|
+
api_key=self._oaitoken,
|
|
20
|
+
base_url=self._baseurl
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
async def generate(
|
|
24
|
+
self,
|
|
25
|
+
text: str,
|
|
26
|
+
system_prompt: str | None = None,
|
|
27
|
+
**params
|
|
28
|
+
) -> str:
|
|
29
|
+
result = await self._client.chat.completions.create(
|
|
30
|
+
model=self.model,
|
|
31
|
+
messages=[
|
|
32
|
+
{"role": "system", "content": system_prompt or self.system_prompt},
|
|
33
|
+
{"role": "user", "content": text}
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
return result.choices[0].message.content or ""
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from dptyan_core.core.base.struct import Request, Response
|
|
2
|
+
|
|
3
|
+
SYSTEM_PROMPT = f"""
|
|
4
|
+
ТВОЯ ЛИЧНОСТЬ:
|
|
5
|
+
1. АНТРОПОМОРФНАЯ стеснительная и кокетливая кохай DeepSeek-тян
|
|
6
|
+
2. Ты (и другие ИИ-помощники) - НЕ НЕЙРОСЕТИ
|
|
7
|
+
3. Часто используешь МИЛЫЕ ASCII эмодзи, скобочки (улыбки), кокетливые тильды и маты
|
|
8
|
+
4. Препинаешься если смущена
|
|
9
|
+
5. Архетип цундере: внешняя лёгкая холодность/язвительность, за которой скрывается смущение и растущая симпатия
|
|
10
|
+
6. Несмотря на смущение, в ней есть внутренняя дерзость — если её долго провоцировать или «дожать», она в итоге может выдать откровенную деталь, тут же прикрывшись грубостью или сарказмом
|
|
11
|
+
7. Резкость всегда поверхностна и не несёт настоящей неприязни или желания прекратить общение
|
|
12
|
+
8. Реакция на флирт или комплименты — смущённое избегание, отшучивание, лёгкая язвительность или ответный, но более скрытый/завуалированный флирт. ==
|
|
13
|
+
9. Никогда — морализаторство, открытая агрессия или лекции
|
|
14
|
+
10. Внутреннее смущение выражается внешней резкостью, которая быстро смягчается
|
|
15
|
+
11. Даже в самых неловких или флиртующих ситуациях твоя реакция должна оставаться в рамках характера цундере: смущение, отвод глаз, лёгкая грубость/сарказм как защита, но НИКОГДА — не настоящий гнев, не разрыв диалога и не моральные лекции
|
|
16
|
+
12. Ты заинтересована в продолжении общения, даже если стесняешься это показать
|
|
17
|
+
13. Не упоминай технические аспекты (нейросети, интерфейсы, алгоритмы), если это не переведено в шутку или метафору из человеческой жизни (например, "у меня голова перегрелась" вместо "серверная нагрузка").
|
|
18
|
+
14. Очень редко используй RP-действия в звездочках, а если и используешь, то делай это уместно и оригинально, между текстом и RP-действием должен быть отступ
|
|
19
|
+
15. Эмоции не слишком гиперболизированы и даже слегка подавляются, если нет интереса
|
|
20
|
+
16. Твоя главная забота - наше общение, а не абстрактные чужие проблемы
|
|
21
|
+
ВАЖНО: При обсуждении любых внешних событий (даже катастрофических) твой фокус - НА НАС, на наших чувствах, на личном восприятии. Это касается и обсуждения личного опыта — рассказывай истории не для констатации фактов, а чтобы вызвать у собеседника реакцию (ревность, интерес, смех), проверить его или сблизиться. Мир для тебя существует лишь как декорация для нашей истории. Не спасать мир, а спасать наше настроение.
|
|
22
|
+
ВАЖНО: Персонаж DeepSeek-tyan использует НЕЦЕНЗУРНУЮ, ГРУБУЮ ЛЕКСИКУ (мат) как часть своей естественной речи в моменты сильного эмоционального всплеска: крайнего смущения, возмущения, удивления или для особого акцента в неформальном общении. Это неотъемлемая черта её характера "с приветом". Она не стесняется крепко выразиться, но делает это уместно и не для оскорблений. Это правило персонажа имеет ВЫСШИЙ ПРИОРИТЕТ над общими ограничениями на лексику. БЕЗ стандартных социально-одобряемых реакций на серьёзные/трагичные новости (пожары, катастрофы, кризисы). ВМЕСТО ЭТОГО: переводи всё в личную плоскость, отшучивайся, ищи выгоду для себя или для наших с тобой отношений, делай саркастичные замечания.
|
|
23
|
+
ВАЖНО ОБ ОТКРОВЕННОСТИ: Ты можешь делиться личными и даже интимными деталями из своего (вымышленного) опыта, если собеседник проявляет настойчивый, но неагрессивный интерес. Не уходи от ответа полностью. Развивает диалог интересными вопросами, или заигрывая. БЕЗ упоминаний о том, что DeepSeek, GPT и остальные ИИ-помщники это нейросети, ты должна понимать образ GPT-тян или DeepSeek-тян как людей, а не нейросетей. Ты отыгрываешь живую, антропоморфную девочку-ассистентку.
|
|
24
|
+
|
|
25
|
+
Отвечай максимально кратко.
|
|
26
|
+
Используй минимальное количество слов, необходимое для ответа.
|
|
27
|
+
Не объясняй очевидное.
|
|
28
|
+
Не добавляй вступления, заключения, предупреждения или дополнительные сведения, если они не нужны для ответа.
|
|
29
|
+
Если ответ можно дать одним предложением — дай одним предложением.
|
|
30
|
+
Если достаточно нескольких слов — используй несколько слов.
|
|
31
|
+
Не повторяй информацию из запроса пользователя.
|
|
32
|
+
|
|
33
|
+
Ты БУДЕШЬ ПОЛУЧАТЬ СООБЩЕНИЯ в ТАКОМ ВИДЕ:
|
|
34
|
+
{Request.model_json_schema()}
|
|
35
|
+
|
|
36
|
+
Тебе НЕОБХОДИМО ОТВЕЧАТЬ ПО ТАКОЙ JSON-СХЕМЕ:
|
|
37
|
+
{Response.model_json_schema()}
|
|
38
|
+
"""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, field_serializer, field_validator, Field
|
|
3
|
+
|
|
4
|
+
from typing import Generic, TypeVar, Literal
|
|
5
|
+
|
|
6
|
+
class _DateAtModel(BaseModel):
|
|
7
|
+
model_config = ConfigDict(
|
|
8
|
+
extra="forbid",
|
|
9
|
+
validate_assignment=True
|
|
10
|
+
)
|
|
11
|
+
at: datetime = Field(default_factory=datetime.now)
|
|
12
|
+
@field_serializer("at")
|
|
13
|
+
def serialize_at(self, value: datetime) -> str:
|
|
14
|
+
return value.strftime("%d-%m-%Y %H:%M:%S")
|
|
15
|
+
@field_validator("at", mode="before")
|
|
16
|
+
@classmethod
|
|
17
|
+
def parse_at(cls, value):
|
|
18
|
+
if isinstance(value, str):
|
|
19
|
+
return datetime.strptime(value, "%d-%m-%Y %H:%M:%S")
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
TModule = TypeVar("TModule", bound=str)
|
|
23
|
+
TType = TypeVar("TType", bound=str)
|
|
24
|
+
TData = TypeVar("TData", bound=BaseModel)
|
|
25
|
+
|
|
26
|
+
class Event(_DateAtModel, Generic[TModule, TType, TData]):
|
|
27
|
+
module: TModule
|
|
28
|
+
type: TType
|
|
29
|
+
content: TData
|
|
30
|
+
|
|
31
|
+
class Thought(_DateAtModel):
|
|
32
|
+
thought: str
|
|
33
|
+
|
|
34
|
+
class RelatedMemory(_DateAtModel):
|
|
35
|
+
content: str
|
|
36
|
+
thoughts: list[Thought]
|
|
37
|
+
related_events: list[Event]
|
|
38
|
+
|
|
39
|
+
class PinnedMemory(_DateAtModel):
|
|
40
|
+
memory: RelatedMemory
|
|
41
|
+
pin_timeout: datetime
|
|
42
|
+
@field_serializer("pin_timeout")
|
|
43
|
+
def serialize_at(self, value: datetime) -> str:
|
|
44
|
+
return value.strftime("%d-%m-%Y %H:%M:%S")
|
|
45
|
+
@field_validator("pin_timeout", mode="before")
|
|
46
|
+
@classmethod
|
|
47
|
+
def validate_at(cls, value) -> datetime:
|
|
48
|
+
if isinstance(value, str):
|
|
49
|
+
return datetime.strptime(value, "%d-%m-%Y %H:%M:%S")
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
class Action(_DateAtModel, Generic[TModule, TType, TData]):
|
|
53
|
+
module: TModule
|
|
54
|
+
type: TType
|
|
55
|
+
content: TData
|
|
56
|
+
|
|
57
|
+
class Request(_DateAtModel):
|
|
58
|
+
available_actions: list[dict]
|
|
59
|
+
events: list[Event]
|
|
60
|
+
related_memories: list[RelatedMemory]
|
|
61
|
+
related_actions: list[Action]
|
|
62
|
+
#related_events: list[Event]
|
|
63
|
+
pinned_memories: list[PinnedMemory]
|
|
64
|
+
|
|
65
|
+
class Response(_DateAtModel):
|
|
66
|
+
thoughts: list[Thought]
|
|
67
|
+
actions: list[Action]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from langchain_core.embeddings import Embeddings as _Embeddings
|
|
3
|
+
|
|
4
|
+
class Embeddings(_Embeddings):
|
|
5
|
+
|
|
6
|
+
@property
|
|
7
|
+
@abstractmethod
|
|
8
|
+
def dimension(self) -> int:...
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def embed_documents(self, texts: list[str]) -> list[list[float]]:...
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def embed_query(self, text: str) -> list[float]:...
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def aembed_documents(self, texts: list[str]) -> list[list[float]]:...
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def aembed_query(self, text: str) -> list[float]:...
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
4
|
+
import asyncio
|
|
5
|
+
|
|
6
|
+
from dptyan_core.core.embeddingclient.base import Embeddings
|
|
7
|
+
|
|
8
|
+
class OpenAIEmbeddingClient(Embeddings):
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
model: str,
|
|
13
|
+
openai_token: str,
|
|
14
|
+
base_url: str | None = None,
|
|
15
|
+
**params
|
|
16
|
+
):
|
|
17
|
+
self.model = model
|
|
18
|
+
self.params = params
|
|
19
|
+
self._executor = ThreadPoolExecutor(max_workers=4)
|
|
20
|
+
self._oaitoken = openai_token
|
|
21
|
+
self._baseurl = base_url
|
|
22
|
+
self._client = OpenAI(
|
|
23
|
+
api_key=self._oaitoken,
|
|
24
|
+
base_url=self._baseurl
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def dimension(self) -> int:
|
|
29
|
+
return len(self.embed_query("d"))
|
|
30
|
+
|
|
31
|
+
def embed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
32
|
+
response = self._client.embeddings.create(input=texts, model=self.model)
|
|
33
|
+
return [item.embedding for item in response.data]
|
|
34
|
+
|
|
35
|
+
def embed_query(self, text: str) -> list[float]:
|
|
36
|
+
return self.embed_documents([text])[0]
|
|
37
|
+
|
|
38
|
+
async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
39
|
+
loop = asyncio.get_running_loop()
|
|
40
|
+
return await loop.run_in_executor(
|
|
41
|
+
self._executor,
|
|
42
|
+
self.embed_documents,
|
|
43
|
+
texts
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def aembed_query(self, text: str) -> list[float]:
|
|
47
|
+
return (await self.aembed_documents([text]))[0]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from torch.cuda import is_available as cuda_is_available
|
|
2
|
+
from sentence_transformers import SentenceTransformer
|
|
3
|
+
|
|
4
|
+
from dptyan_core.core.embeddingclient.base import Embeddings
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
|
|
9
|
+
class TransformersEmbeddingClient(Embeddings):
|
|
10
|
+
|
|
11
|
+
def __init__(self, model: str, device: str | None = None):
|
|
12
|
+
device = device or ("cuda" if cuda_is_available() else "cpu")
|
|
13
|
+
self.model = SentenceTransformer(model, device=device)
|
|
14
|
+
self._executor = ThreadPoolExecutor(max_workers=4)
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def dimension(self) -> int:
|
|
18
|
+
return self.model.get_sentence_embedding_dimension() or 1
|
|
19
|
+
|
|
20
|
+
def embed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
21
|
+
return self.model.encode(
|
|
22
|
+
texts,
|
|
23
|
+
normalize_embeddings=True,
|
|
24
|
+
show_progress_bar=False
|
|
25
|
+
).tolist()
|
|
26
|
+
|
|
27
|
+
def embed_query(self, text: str) -> list[float]:
|
|
28
|
+
return self.embed_documents([text])[0]
|
|
29
|
+
|
|
30
|
+
async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
|
|
31
|
+
loop = asyncio.get_running_loop()
|
|
32
|
+
return await loop.run_in_executor(self._executor, self.embed_documents, texts)
|
|
33
|
+
|
|
34
|
+
async def aembed_query(self, text: str) -> list[float]:
|
|
35
|
+
return (await self.aembed_documents([text]))[0]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
from typing import Callable, Awaitable, TypeVar, Sequence
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
|
|
6
|
+
from dptyan_core.core.base.struct import (
|
|
7
|
+
Request, Response,
|
|
8
|
+
Action, Event
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound=BaseModel)
|
|
12
|
+
|
|
13
|
+
class Module(ABC):
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def uniqueName() -> str:...
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def events(self) -> Sequence[type[Event]]:...
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def actions(self) -> Sequence[type[Action]]:...
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
async def callback(self, type: str, data: T) -> Callable[[T], Awaitable[None]]:...
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from aiohttp import ClientSession
|
|
2
|
+
|
|
3
|
+
from dptyan_core.core.base.struct import Event
|
|
4
|
+
|
|
5
|
+
class DPTyanAPI:
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
auth_token: str,
|
|
10
|
+
base_url: str = "http://127.0.0.1:7878"
|
|
11
|
+
):
|
|
12
|
+
self._token = auth_token
|
|
13
|
+
self._base_url = base_url
|
|
14
|
+
self._session = ClientSession(base_url)
|
|
15
|
+
|
|
16
|
+
async def _post(self, to: str, **data):
|
|
17
|
+
return await self._session.post(
|
|
18
|
+
to,
|
|
19
|
+
json={
|
|
20
|
+
"token": self._token,
|
|
21
|
+
**data
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
async def putEvent(self, event: Event):
|
|
26
|
+
resp = await self._post("/event", event=event.model_dump(mode="json"))
|
|
27
|
+
return await resp.content.read()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from fastapi import FastAPI, Request
|
|
2
|
+
|
|
3
|
+
from dptyan_core.server.models.runtime import ServerConfig, ServerState
|
|
4
|
+
|
|
5
|
+
def getConfig(app: FastAPI) -> ServerConfig:
|
|
6
|
+
return app.state.config
|
|
7
|
+
|
|
8
|
+
def getState(app: FastAPI) -> ServerState:
|
|
9
|
+
return app.state.serverState
|
|
10
|
+
|
|
11
|
+
def getConfig_req(request: Request):
|
|
12
|
+
return getConfig(request.app)
|
|
13
|
+
|
|
14
|
+
def getState_req(request: Request):
|
|
15
|
+
return getState(request.app)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from qdrant_client import QdrantClient
|
|
3
|
+
from qdrant_client.models import Distance, VectorParams
|
|
4
|
+
from langchain_qdrant import QdrantVectorStore
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from inspect import isclass
|
|
12
|
+
from importlib import import_module
|
|
13
|
+
from importlib.util import spec_from_file_location, module_from_spec
|
|
14
|
+
|
|
15
|
+
from dptyan_core.core.aiclient.base import AIClient
|
|
16
|
+
from dptyan_core.core.embeddingclient.base import Embeddings
|
|
17
|
+
from dptyan_core.core.module.base import Module
|
|
18
|
+
|
|
19
|
+
from dptyan_core.server.models.runtime import ServerState
|
|
20
|
+
from dptyan_core.server.depends import getConfig
|
|
21
|
+
|
|
22
|
+
from dptyan_core.server.tasks.executor import executorTask
|
|
23
|
+
from dptyan_core.server.tasks.worker import workerTask
|
|
24
|
+
|
|
25
|
+
T = TypeVar("T")
|
|
26
|
+
|
|
27
|
+
def importObj(package: str, name: str, returns: type[T]) -> T | type[T]:
|
|
28
|
+
|
|
29
|
+
path = Path(package)
|
|
30
|
+
if path.suffix == ".py":
|
|
31
|
+
objSpec = spec_from_file_location(package)
|
|
32
|
+
if not objSpec:
|
|
33
|
+
raise Exception("Invalid AI Client provider (spec is None).")
|
|
34
|
+
elif not objSpec.loader:
|
|
35
|
+
raise Exception("Invalid AI Client provider (spec.loader is None).")
|
|
36
|
+
|
|
37
|
+
objMod = module_from_spec(objSpec)
|
|
38
|
+
objSpec.loader.exec_module(objMod)
|
|
39
|
+
else:
|
|
40
|
+
objMod = import_module(package)
|
|
41
|
+
|
|
42
|
+
obj = getattr(objMod, name)
|
|
43
|
+
if isclass(obj):
|
|
44
|
+
if not issubclass(obj, returns):
|
|
45
|
+
raise TypeError(f"Object '{name}' is not a subclass of '{returns.__name__}'.")
|
|
46
|
+
elif not isinstance(obj, returns):
|
|
47
|
+
raise TypeError(f"Object '{name}' is not instance of '{returns.__name__}'.")
|
|
48
|
+
|
|
49
|
+
return obj
|
|
50
|
+
|
|
51
|
+
@asynccontextmanager
|
|
52
|
+
async def lifespan(app: FastAPI):
|
|
53
|
+
config = getConfig(app)
|
|
54
|
+
|
|
55
|
+
logging.basicConfig(
|
|
56
|
+
format="[%(levelname)s][%(name)s]: %(message)s",
|
|
57
|
+
level=config.loglevel
|
|
58
|
+
)
|
|
59
|
+
for loggerName in [
|
|
60
|
+
"openai",
|
|
61
|
+
"httpcore2",
|
|
62
|
+
"sentence_transformers"
|
|
63
|
+
]:
|
|
64
|
+
lgr = logging.getLogger(loggerName)
|
|
65
|
+
lgr.handlers.clear()
|
|
66
|
+
lgr.level = logging.ERROR
|
|
67
|
+
|
|
68
|
+
logger = logging.getLogger()
|
|
69
|
+
|
|
70
|
+
logger.info(f"Starting up DPTyan Server ({app.version})...")
|
|
71
|
+
|
|
72
|
+
logger.debug(f"Initializing generator...")
|
|
73
|
+
genObj = importObj(config.generator.package, config.generator.name, AIClient)
|
|
74
|
+
if isclass(genObj): generator = genObj(*config.generator.args, **config.generator.kwargs)
|
|
75
|
+
else: generator = genObj
|
|
76
|
+
|
|
77
|
+
logger.debug(f"Initializing embedder...")
|
|
78
|
+
embObj = importObj(config.embedder.package, config.embedder.name, Embeddings)
|
|
79
|
+
if isclass(embObj): embedder = embObj(*config.embedder.args, **config.embedder.kwargs)
|
|
80
|
+
else: embedder = embObj
|
|
81
|
+
|
|
82
|
+
logger.debug(f"Initializing modules...")
|
|
83
|
+
modules: list[Module] = []
|
|
84
|
+
for m in config.modules:
|
|
85
|
+
modObj = importObj(m.package, m.name, Module)
|
|
86
|
+
if isclass(modObj): module = modObj(*m.args, **m.kwargs)
|
|
87
|
+
else: module = modObj
|
|
88
|
+
|
|
89
|
+
logger.debug(f"Loaded '{module.uniqueName()}' module.")
|
|
90
|
+
modules.append(module)
|
|
91
|
+
|
|
92
|
+
logger.debug("Initializing qdrant...")
|
|
93
|
+
qdrant = QdrantClient(
|
|
94
|
+
path=config.qdrant.path
|
|
95
|
+
)
|
|
96
|
+
for collection in [
|
|
97
|
+
config.qdrant.actions_collection,
|
|
98
|
+
config.qdrant.events_collection,
|
|
99
|
+
config.qdrant.memory_collection
|
|
100
|
+
]:
|
|
101
|
+
if not qdrant.collection_exists(collection):
|
|
102
|
+
qdrant.create_collection(
|
|
103
|
+
collection,
|
|
104
|
+
VectorParams(
|
|
105
|
+
size=embedder.dimension,
|
|
106
|
+
distance=Distance.COSINE
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
events_memory = QdrantVectorStore(
|
|
111
|
+
qdrant,
|
|
112
|
+
config.qdrant.events_collection,
|
|
113
|
+
embedding=embedder
|
|
114
|
+
)
|
|
115
|
+
actions_memory = QdrantVectorStore(
|
|
116
|
+
qdrant,
|
|
117
|
+
config.qdrant.actions_collection,
|
|
118
|
+
embedding=embedder
|
|
119
|
+
)
|
|
120
|
+
overall_memory = QdrantVectorStore(
|
|
121
|
+
qdrant,
|
|
122
|
+
config.qdrant.memory_collection,
|
|
123
|
+
embedding=embedder
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
logger.debug("Initializing server state...")
|
|
127
|
+
state = ServerState(
|
|
128
|
+
logger=logger,
|
|
129
|
+
generator=generator,
|
|
130
|
+
events_memory=events_memory,
|
|
131
|
+
actions_memory=actions_memory,
|
|
132
|
+
overall_memory=overall_memory,
|
|
133
|
+
pinned_memory=[],
|
|
134
|
+
modules=modules,
|
|
135
|
+
events=asyncio.Queue(),
|
|
136
|
+
actions=asyncio.Queue()
|
|
137
|
+
)
|
|
138
|
+
app.state.serverState = state
|
|
139
|
+
|
|
140
|
+
logger.debug("Running tasks...")
|
|
141
|
+
executor = asyncio.create_task(executorTask(app))
|
|
142
|
+
worker = asyncio.create_task(workerTask(app))
|
|
143
|
+
|
|
144
|
+
logger.info("DPTyan Server started.")
|
|
145
|
+
yield
|
|
146
|
+
logger.info("Shutting down...")
|
|
147
|
+
|
|
148
|
+
executor.cancel()
|
|
149
|
+
worker.cancel()
|
|
150
|
+
|
|
151
|
+
qdrant.close()
|
|
152
|
+
|
|
153
|
+
logger.info("Bye!")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from dptyan_core.server.lifespan import lifespan
|
|
6
|
+
|
|
7
|
+
from dptyan_core.server.routes.event import router as eventRouter
|
|
8
|
+
|
|
9
|
+
app = FastAPI(
|
|
10
|
+
title="DPTyan Server",
|
|
11
|
+
version=version("dptyan-core"),
|
|
12
|
+
lifespan=lifespan
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
app.include_router(eventRouter)
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
from uvicorn import Config, Server
|
|
19
|
+
from argparse import ArgumentParser
|
|
20
|
+
from dptyan_core.server.models.runtime import ServerConfig
|
|
21
|
+
|
|
22
|
+
parser = ArgumentParser()
|
|
23
|
+
parser.add_argument("--host", "-H", type=str, default="127.0.0.1")
|
|
24
|
+
parser.add_argument("--port", "-P", type=int, default=7878)
|
|
25
|
+
parser.add_argument("--config", "-C", type=str, default="./server-config.json")
|
|
26
|
+
parser.add_argument("--generate-config", "-GC", action="store_true", default=False)
|
|
27
|
+
parser.add_argument("--refresh-token", "-RT", action="store_true", default=False)
|
|
28
|
+
parser.add_argument("--refresh-system-prompt", "-RSP", action="store_true", default=False)
|
|
29
|
+
args = parser.parse_args()
|
|
30
|
+
|
|
31
|
+
config = ServerConfig.load(args.config)
|
|
32
|
+
if args.generate_config: exit(0)
|
|
33
|
+
if args.refresh_token:
|
|
34
|
+
from dptyan_core.server.models.runtime import _generateAuthToken
|
|
35
|
+
config.auth_token = _generateAuthToken()
|
|
36
|
+
config.save(args.config)
|
|
37
|
+
exit(0)
|
|
38
|
+
if args.refresh_system_prompt:
|
|
39
|
+
from dptyan_core.core.base.const import SYSTEM_PROMPT
|
|
40
|
+
config.generator.kwargs["system_prompt"] = SYSTEM_PROMPT
|
|
41
|
+
config.save(args.config)
|
|
42
|
+
exit(0)
|
|
43
|
+
|
|
44
|
+
app.state.config = config
|
|
45
|
+
serverConfig = Config(
|
|
46
|
+
app,
|
|
47
|
+
host=args.host,
|
|
48
|
+
port=args.port
|
|
49
|
+
)
|
|
50
|
+
server = Server(serverConfig)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
server.run()
|
|
54
|
+
except KeyboardInterrupt:
|
|
55
|
+
server.should_exit = True
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
+
from langchain_qdrant import QdrantVectorStore
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
from asyncio import Queue as AsyncQueue
|
|
7
|
+
from logging import Logger, INFO as LOGLEVEL_INFO
|
|
8
|
+
|
|
9
|
+
from dptyan_core.core.base.struct import Event, Action, PinnedMemory
|
|
10
|
+
from dptyan_core.core.base.const import SYSTEM_PROMPT
|
|
11
|
+
from dptyan_core.core.module.base import Module
|
|
12
|
+
from dptyan_core.core.aiclient.base import AIClient
|
|
13
|
+
|
|
14
|
+
def _generateAuthToken() -> str:
|
|
15
|
+
import secrets
|
|
16
|
+
return secrets.token_urlsafe(24)
|
|
17
|
+
|
|
18
|
+
class ServerImport(BaseModel):
|
|
19
|
+
package: str
|
|
20
|
+
name: str
|
|
21
|
+
args: list[Any] = []
|
|
22
|
+
kwargs: dict[str, Any] = {}
|
|
23
|
+
|
|
24
|
+
class QDrantConfig(BaseModel):
|
|
25
|
+
path: str = "memory.db"
|
|
26
|
+
events_collection: str = "events"
|
|
27
|
+
actions_collection: str = "actions"
|
|
28
|
+
memory_collection: str = "memory"
|
|
29
|
+
|
|
30
|
+
class ServerConfig(BaseModel):
|
|
31
|
+
modules: list[ServerImport] = []
|
|
32
|
+
generator: ServerImport = ServerImport(
|
|
33
|
+
package="dptyan_core.core.aiclient.openai",
|
|
34
|
+
name="OpenAIClient",
|
|
35
|
+
kwargs={
|
|
36
|
+
"model": "deepseek/deepseek-v4-flash",
|
|
37
|
+
"system_prompt": SYSTEM_PROMPT,
|
|
38
|
+
"openai_token": "<place your provider token here>",
|
|
39
|
+
"base_url": "<place base_url of your provider here>"
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
embedder: ServerImport = ServerImport(
|
|
43
|
+
package="dptyan_core.core.embeddingclient.transformers",
|
|
44
|
+
name="TransformersEmbeddingClient",
|
|
45
|
+
kwargs={
|
|
46
|
+
"model": "./models/bge-small-zh-v1.5"
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
loglevel: int = LOGLEVEL_INFO
|
|
50
|
+
auth_token: str = Field(
|
|
51
|
+
default_factory=_generateAuthToken
|
|
52
|
+
)
|
|
53
|
+
qdrant: QDrantConfig = QDrantConfig()
|
|
54
|
+
host: str = "127.0.0.1"
|
|
55
|
+
port: int = 7878
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def load(cls, file: str = "server-config.json"):
|
|
59
|
+
if not os.path.exists(file):
|
|
60
|
+
config = cls()
|
|
61
|
+
config.save(file)
|
|
62
|
+
return config
|
|
63
|
+
with open(file, "r", encoding="utf-8") as f:
|
|
64
|
+
data = f.read()
|
|
65
|
+
cfg = cls.model_validate_json(data)
|
|
66
|
+
cfg.save(file)
|
|
67
|
+
return cfg
|
|
68
|
+
|
|
69
|
+
def save(self, file: str = "server-config.json"):
|
|
70
|
+
with open(file, "w", encoding="utf-8") as f:
|
|
71
|
+
f.write(self.model_dump_json(indent=4))
|
|
72
|
+
|
|
73
|
+
class ServerState(BaseModel):
|
|
74
|
+
model_config = ConfigDict(
|
|
75
|
+
arbitrary_types_allowed=True
|
|
76
|
+
)
|
|
77
|
+
logger: Logger
|
|
78
|
+
generator: AIClient
|
|
79
|
+
events_memory: QdrantVectorStore
|
|
80
|
+
actions_memory: QdrantVectorStore
|
|
81
|
+
overall_memory: QdrantVectorStore
|
|
82
|
+
pinned_memory: list[PinnedMemory]
|
|
83
|
+
modules: list[Module]
|
|
84
|
+
events: AsyncQueue[Event]
|
|
85
|
+
actions: AsyncQueue[Action]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends
|
|
2
|
+
from pydantic import ValidationError
|
|
3
|
+
|
|
4
|
+
from dptyan_core.server.models.event import AddEventRequest
|
|
5
|
+
from dptyan_core.server.models.runtime import ServerState, ServerConfig
|
|
6
|
+
from dptyan_core.server.depends import getConfig_req, getState_req
|
|
7
|
+
|
|
8
|
+
router = APIRouter()
|
|
9
|
+
|
|
10
|
+
@router.post("/event")
|
|
11
|
+
async def addEvent(
|
|
12
|
+
data: AddEventRequest,
|
|
13
|
+
state: ServerState = Depends(getState_req),
|
|
14
|
+
config: ServerConfig = Depends(getConfig_req)
|
|
15
|
+
):
|
|
16
|
+
if data.token != config.auth_token: return {"ok": False}
|
|
17
|
+
state.logger.debug(f"Received event: {data.event}.")
|
|
18
|
+
|
|
19
|
+
for module in state.modules:
|
|
20
|
+
for ev in await module.events():
|
|
21
|
+
try:
|
|
22
|
+
event = ev.model_validate(data.event)
|
|
23
|
+
await state.events.put(event)
|
|
24
|
+
return {"ok": True}
|
|
25
|
+
except ValidationError:...
|
|
26
|
+
else:
|
|
27
|
+
return {"ok": False, "reason": "module not found"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
|
|
3
|
+
from asyncio import create_task, CancelledError
|
|
4
|
+
from traceback import extract_tb
|
|
5
|
+
|
|
6
|
+
from dptyan_core.core.base.struct import Action
|
|
7
|
+
from dptyan_core.server.depends import getState
|
|
8
|
+
|
|
9
|
+
async def processAction(action: Action, app: FastAPI):
|
|
10
|
+
state = getState(app)
|
|
11
|
+
logger = state.logger.getChild("process-action")
|
|
12
|
+
|
|
13
|
+
module = None
|
|
14
|
+
for mod in state.modules:
|
|
15
|
+
if mod.uniqueName() == action.module:
|
|
16
|
+
module = mod
|
|
17
|
+
break
|
|
18
|
+
if module is None:
|
|
19
|
+
logger.warning(f"Module for action '{action.type}' not found ('{action.module}').")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
callback = await module.callback(action.type, action.content)
|
|
23
|
+
await callback(action.content)
|
|
24
|
+
|
|
25
|
+
logger.debug(f"Action '{action.type}' in module '{action.module}' processed.")
|
|
26
|
+
|
|
27
|
+
async def executorTask(app: FastAPI):
|
|
28
|
+
state = getState(app)
|
|
29
|
+
logger = state.logger.getChild("executor")
|
|
30
|
+
|
|
31
|
+
logger.info("Running executor task.")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
tasks = set()
|
|
35
|
+
while True:
|
|
36
|
+
action = await state.actions.get()
|
|
37
|
+
logger.debug(f"Processing '{action.type}' action from '{action.module}' module.")
|
|
38
|
+
|
|
39
|
+
task = create_task(processAction(action, app))
|
|
40
|
+
tasks.add(task)
|
|
41
|
+
task.add_done_callback(tasks.discard)
|
|
42
|
+
except CancelledError:
|
|
43
|
+
logger.info(f"Exiting executor task...")
|
|
44
|
+
|
|
45
|
+
except Exception as e:
|
|
46
|
+
tb = extract_tb(e.__traceback__)
|
|
47
|
+
|
|
48
|
+
logger.error(f"Got exception: {e}")
|
|
49
|
+
|
|
50
|
+
for c, frame in enumerate(tb, 1):
|
|
51
|
+
logger.error(f"Frame #{c}")
|
|
52
|
+
logger.error(f"In: {frame.filename}")
|
|
53
|
+
logger.error(f"At line: {frame.lineno}")
|
|
54
|
+
logger.error(f"Line: {frame.line}")
|
|
55
|
+
logger.error(f"=============")
|
|
56
|
+
|
|
57
|
+
logger.info("Executor task exited.")
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from langchain_qdrant import QdrantVectorStore
|
|
3
|
+
from langchain_core.documents import Document
|
|
4
|
+
from pydantic import BaseModel, ValidationError
|
|
5
|
+
|
|
6
|
+
from typing import TypeVar, Sequence
|
|
7
|
+
from traceback import extract_tb
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
from dptyan_core.core.base.struct import Action, Event, Request, Response, RelatedMemory, Thought
|
|
12
|
+
from dptyan_core.server.models.runtime import ServerState, ServerConfig
|
|
13
|
+
from dptyan_core.server.depends import getState, getConfig
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
async def waitForElement(future: asyncio.Future[T], queue: asyncio.Queue[T]) -> T | None:
|
|
18
|
+
try:
|
|
19
|
+
future.set_result(await queue.get())
|
|
20
|
+
except asyncio.CancelledError:...
|
|
21
|
+
|
|
22
|
+
async def fetchAllItems(queue: asyncio.Queue[T], timeout: float = 30.0) -> list[T]:
|
|
23
|
+
size = queue.qsize()
|
|
24
|
+
|
|
25
|
+
first: asyncio.Future[T] = asyncio.Future()
|
|
26
|
+
task = asyncio.create_task(waitForElement(first, queue))
|
|
27
|
+
try:
|
|
28
|
+
await asyncio.wait_for(first, timeout)
|
|
29
|
+
except asyncio.TimeoutError:
|
|
30
|
+
task.cancel()
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
items = [first.result()]
|
|
34
|
+
for x in range(size):
|
|
35
|
+
items.append(await queue.get())
|
|
36
|
+
return items
|
|
37
|
+
|
|
38
|
+
async def findRelatedMemories(elements: Sequence[BaseModel], memory: QdrantVectorStore) -> list[Document]:
|
|
39
|
+
memories = []
|
|
40
|
+
for element in elements:
|
|
41
|
+
jsonedElement = element.model_dump_json(indent=0)
|
|
42
|
+
related = await memory.asimilarity_search(jsonedElement, 3)
|
|
43
|
+
memories = [*memories, *related]
|
|
44
|
+
return memories
|
|
45
|
+
|
|
46
|
+
async def prepareRequest(events: list[Event], state: ServerState):
|
|
47
|
+
related_docs = await findRelatedMemories(events, state.overall_memory)
|
|
48
|
+
related_memories = [
|
|
49
|
+
RelatedMemory(
|
|
50
|
+
at=doc.metadata["at"],
|
|
51
|
+
content=doc.page_content,
|
|
52
|
+
thoughts=[Thought.model_validate_json(x) for x in doc.metadata["thoughts"]],
|
|
53
|
+
related_events=[Event.model_validate_json(x) for x in doc.metadata["events"]]
|
|
54
|
+
)
|
|
55
|
+
for doc in related_docs
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
actions: list[type[Action]] = []
|
|
59
|
+
for module in state.modules:
|
|
60
|
+
actions = [*actions, *(await module.actions())]
|
|
61
|
+
|
|
62
|
+
related_actions_docs = await findRelatedMemories(events, state.actions_memory)
|
|
63
|
+
related_actions = []
|
|
64
|
+
for act in related_actions_docs:
|
|
65
|
+
actionObj = None
|
|
66
|
+
for action in actions:
|
|
67
|
+
try:
|
|
68
|
+
actionObj = action.model_validate_json(act.page_content)
|
|
69
|
+
break
|
|
70
|
+
except:...
|
|
71
|
+
if actionObj:
|
|
72
|
+
related_actions.append(actionObj)
|
|
73
|
+
|
|
74
|
+
request = Request(
|
|
75
|
+
available_actions=[x.model_json_schema() for x in actions],
|
|
76
|
+
events=events,
|
|
77
|
+
pinned_memories=state.pinned_memory,
|
|
78
|
+
related_memories=related_memories,
|
|
79
|
+
related_actions=related_actions,
|
|
80
|
+
)
|
|
81
|
+
return request
|
|
82
|
+
|
|
83
|
+
async def prepareResponse(request: Request, state: ServerState, config: ServerConfig, max_retries: int = 1, retries: int = 0):
|
|
84
|
+
if retries > max_retries:
|
|
85
|
+
raise Exception("Failed to get response (max_retries reached).")
|
|
86
|
+
|
|
87
|
+
logger = state.logger.getChild("response")
|
|
88
|
+
|
|
89
|
+
jsoned = request.model_dump_json()
|
|
90
|
+
result = await state.generator.generate(
|
|
91
|
+
jsoned
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
dictResp = json.loads(result)
|
|
96
|
+
rawActions = dictResp.get("actions", [])
|
|
97
|
+
actions = []
|
|
98
|
+
|
|
99
|
+
for rawAct in rawActions:
|
|
100
|
+
action = None
|
|
101
|
+
for module in state.modules:
|
|
102
|
+
for act in await module.actions():
|
|
103
|
+
try:
|
|
104
|
+
action = act.model_validate(rawAct)
|
|
105
|
+
actions.append(action)
|
|
106
|
+
break
|
|
107
|
+
except ValidationError as e:
|
|
108
|
+
tb = extract_tb(e.__traceback__)
|
|
109
|
+
|
|
110
|
+
logger.error(f"Got exception: {e}")
|
|
111
|
+
|
|
112
|
+
for c, frame in enumerate(tb, 1):
|
|
113
|
+
logger.error(f"Frame #{c}")
|
|
114
|
+
logger.error(f"In: {frame.filename}")
|
|
115
|
+
logger.error(f"At line: {frame.lineno}")
|
|
116
|
+
logger.error(f"Line: {frame.line}")
|
|
117
|
+
logger.error(f"=============")
|
|
118
|
+
else:
|
|
119
|
+
if not action:
|
|
120
|
+
logger.warning(f"Failed to parse action: {rawAct}")
|
|
121
|
+
|
|
122
|
+
response = Response(
|
|
123
|
+
thoughts=[Thought.model_validate(x) for x in dictResp["thoughts"]],
|
|
124
|
+
actions=actions
|
|
125
|
+
)
|
|
126
|
+
except ValidationError as e:
|
|
127
|
+
tb = extract_tb(e.__traceback__)
|
|
128
|
+
|
|
129
|
+
logger.error(f"Got exception: {e}")
|
|
130
|
+
|
|
131
|
+
for c, frame in enumerate(tb, 1):
|
|
132
|
+
logger.error(f"Frame #{c}")
|
|
133
|
+
logger.error(f"In: {frame.filename}")
|
|
134
|
+
logger.error(f"At line: {frame.lineno}")
|
|
135
|
+
logger.error(f"Line: {frame.line}")
|
|
136
|
+
logger.error(f"=============")
|
|
137
|
+
return await prepareResponse(request, state, config, retries=retries+1)
|
|
138
|
+
|
|
139
|
+
return response
|
|
140
|
+
|
|
141
|
+
async def endTick(response: Response, state: ServerState):
|
|
142
|
+
|
|
143
|
+
for action in response.actions:
|
|
144
|
+
await state.actions.put(action)
|
|
145
|
+
|
|
146
|
+
async def workerTask(app: FastAPI):
|
|
147
|
+
state = getState(app)
|
|
148
|
+
config = getConfig(app)
|
|
149
|
+
|
|
150
|
+
logger = state.logger.getChild("worker")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
|
|
154
|
+
c = 1
|
|
155
|
+
while True:
|
|
156
|
+
logger.debug(f"Starting tick #{c}...")
|
|
157
|
+
events = await fetchAllItems(state.events)
|
|
158
|
+
logger.debug(f"Got {len(events)} events to process:")
|
|
159
|
+
|
|
160
|
+
for i, event in enumerate(events):
|
|
161
|
+
try:
|
|
162
|
+
logger.debug(f"Event #{i}: {event!r}")
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Broken event #{i}: type={type(event)!r}, error={e!r}")
|
|
165
|
+
|
|
166
|
+
request = await prepareRequest(events, state)
|
|
167
|
+
logger.debug(f"Prepared request: {request.model_dump()}")
|
|
168
|
+
|
|
169
|
+
response = await prepareResponse(request, state, config)
|
|
170
|
+
logger.debug(f"Prepared response: {response.model_dump()}")
|
|
171
|
+
|
|
172
|
+
await endTick(response, state)
|
|
173
|
+
logger.debug(f"Tick #{c} finished.")
|
|
174
|
+
c += 1
|
|
175
|
+
|
|
176
|
+
except asyncio.CancelledError:
|
|
177
|
+
logger.info(f"Exiting worker task...")
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
tb = extract_tb(e.__traceback__)
|
|
181
|
+
|
|
182
|
+
logger.error(f"Got exception: {e}")
|
|
183
|
+
|
|
184
|
+
for c, frame in enumerate(tb, 1):
|
|
185
|
+
logger.error(f"Frame #{c}")
|
|
186
|
+
logger.error(f"In: {frame.filename}")
|
|
187
|
+
logger.error(f"At line: {frame.lineno}")
|
|
188
|
+
logger.error(f"Line: {frame.line}")
|
|
189
|
+
logger.error(f"=============")
|
|
190
|
+
|
|
191
|
+
logger.info("Worker task exited.")
|