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.
@@ -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,9 @@
1
+ from pydantic import BaseModel
2
+
3
+ from dptyan_core.core.base.struct import Event
4
+
5
+ class Request(BaseModel):
6
+ token: str
7
+
8
+ class AddEventRequest(Request):
9
+ event: dict
@@ -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.")