cycls 0.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.
cycls-0.0.1.0/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2018 The Python Packaging Authority
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
cycls-0.0.1.0/PKG-INFO ADDED
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.1
2
+ Name: cycls
3
+ Version: 0.0.1.0
4
+ Summary: Publish your chat app with cycls
5
+ Author: Khalid Alrasheed
6
+ Author-email: khalid@cycls.io
7
+ Requires-Python: >=3.8,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.8
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Requires-Dist: asyncio (>=3.4.3,<4.0.0)
14
+ Requires-Dist: certifi (>=2023.11.17,<2024.0.0)
15
+ Requires-Dist: httpx (>=0.25.2,<0.26.0)
16
+ Requires-Dist: pydantic (>=2.5.2,<3.0.0)
17
+ Requires-Dist: python-socketio[asyncio-client] (>=5.10.0,<6.0.0)
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+ from enum import Enum
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel
6
+
7
+ class UITypes(str, Enum):
8
+ Text = "text"
9
+ Image = "image"
10
+
11
+ class Text(BaseModel):
12
+ text: str
13
+ type:UITypes = UITypes.Text
14
+ meta:dict[str, Any] | None = None
15
+
16
+ def __init__(self, text:str, type:UITypes = UITypes.Text, meta=None):
17
+ super().__init__(text=text, meta=meta)
18
+
19
+ class Image(BaseModel):
20
+ image : str
21
+ type :UITypes = UITypes.Image
22
+ meta : dict[str, Any] | None = None
23
+
24
+ def __init__(self, image_url:str, type:UITypes = UITypes.Image, meta=None):
25
+ super().__init__(Image=image_url, meta=meta)
26
+
@@ -0,0 +1,3 @@
1
+ from .client import Cycls
2
+ from .typings import Response
3
+ from .configuration import AppConfiguration
@@ -0,0 +1,160 @@
1
+ from __future__ import annotations
2
+ from inspect import iscoroutinefunction, signature, Parameter, _empty
3
+ from typing import Callable, Any
4
+ from datetime import datetime
5
+
6
+
7
+ from socketio import AsyncClient
8
+ import asyncio
9
+ import re
10
+
11
+ from .UI import Text, Image
12
+ from .configuration import AppConfiguration
13
+ from .typings import (
14
+ InputTypeHint,
15
+ UserMessage,
16
+ ConversationID,
17
+ ConversationSession,
18
+ Meta,
19
+ Response,
20
+ Message,
21
+ MessageContent
22
+ )
23
+ from .static import HANDLER_PATTERN, CYCLS_URL
24
+
25
+
26
+ class Cycls:
27
+ def __init__(self, key: str | None = None):
28
+ self.key = key
29
+ self.sio = AsyncClient(
30
+ reconnection_attempts=0,
31
+ reconnection_delay=1,
32
+ reconnection_delay_max=25,
33
+ logger=True,
34
+ )
35
+ self.apps_config: list[AppConfiguration] = []
36
+ self.sio.on("connect")(self.re_connect)
37
+ self.sio.on("connection_log")(self.connection_log)
38
+
39
+
40
+ async def re_connect(self):
41
+ for app in self.apps_config:
42
+ await self.sio.emit("connect_app", data=app.model_dump(mode="json"))
43
+ print("CONNECTING")
44
+
45
+
46
+ async def _run(self):
47
+ headers = {
48
+ "x-dev-secret": self.key,
49
+ }
50
+ await self.sio.connect(
51
+ CYCLS_URL,
52
+ headers=headers,
53
+ transports=["websocket"],
54
+ socketio_path="/app-socket/socket.io",
55
+ )
56
+ await self.sio.wait()
57
+
58
+
59
+ def publish(self):
60
+ asyncio.run(self._run())
61
+
62
+ def _process_response(self, response):
63
+ if isinstance(response, Response):
64
+ return response
65
+ elif isinstance(response, Text) or isinstance(response, Image):
66
+ return Response(messages=[response])
67
+ elif isinstance(response, list):
68
+ return Response(messages=response)
69
+ return Response(messages=[response])
70
+
71
+ def _parameter_type_hint(self, param: Parameter) -> InputTypeHint:
72
+ hint = param.annotation
73
+ mapping = {
74
+ _empty: InputTypeHint.EMPTY,
75
+ Message: InputTypeHint.MESSAGE,
76
+ ConversationSession: InputTypeHint.SESSION,
77
+ ConversationID: InputTypeHint.CONVERSATION_ID,
78
+ UserMessage: InputTypeHint.FULL,
79
+ MessageContent: InputTypeHint.MESSAGE_CONTENT,
80
+ }
81
+ if output := mapping.get(hint):
82
+ return output
83
+ else:
84
+ raise Exception("")
85
+
86
+ def _get_parameter_value(self, hint: InputTypeHint, obj: user_message) -> Any:
87
+ if hint == InputTypeHint.MESSAGE_CONTENT:
88
+ return obj.message.content
89
+ elif hint == InputTypeHint.SESSION:
90
+ return obj.session
91
+ elif hint == InputTypeHint.CONVERSATION_ID:
92
+ return obj.session.id
93
+ elif hint == InputTypeHint.MESSAGE:
94
+ return obj.message
95
+ elif hint == InputTypeHint.USER:
96
+ return None
97
+ elif hint == InputTypeHint.EMPTY or hint == InputTypeHint.FULL:
98
+ return obj
99
+
100
+ def process_handler_input(self, func: Callable, message: UserMessage):
101
+ kwargs = {}
102
+ for key, value in signature(func).parameters.items():
103
+ type_hint = self._parameter_type_hint(value)
104
+ kwargs[key] = self._get_parameter_value(hint=type_hint, obj=message)
105
+ return kwargs
106
+
107
+ def extract_handler_name(self, handler: str) -> str:
108
+ name = re.search(rf"^\@({HANDLER_PATTERN})$", handler.strip().lower())
109
+ if not name:
110
+ raise Exception(
111
+ "Your app handler has to start with @ and composed only of letters, numbers and '-'"
112
+ )
113
+ name = name.group(1)
114
+ return re.sub(r"_", "-", name)
115
+
116
+ def app(
117
+ self,
118
+ handler: str,
119
+ name: str | None = None,
120
+ image: str | None = None,
121
+ introduction: str| None = None,
122
+ suggestions: list[str] | None = None
123
+ ):
124
+ """ """
125
+ app_handler = self.extract_handler_name(handler)
126
+ config = AppConfiguration(
127
+ handler=app_handler,
128
+ name=name,
129
+ image=image,
130
+ introduction=introduction,
131
+ suggestions=suggestions
132
+
133
+ )
134
+
135
+ def decorator(func):
136
+ self.apps_config.append(config)
137
+
138
+ @self.sio.on(app_handler)
139
+ async def wrapper(data):
140
+ try:
141
+ message = UserMessage(sio=self.sio, **data)
142
+ except Exception as e:
143
+ print(e)
144
+ print(f"we got error while trying to process {data}")
145
+ return Response(messages=[Text("something went wrong")]).model_dump(
146
+ mode="json"
147
+ )
148
+
149
+ await func(**self.process_handler_input(func, message))
150
+ await message.send.send_message(None, message.send.user_message_id, True, "finish")
151
+ return 200
152
+
153
+ return wrapper
154
+
155
+ return decorator
156
+
157
+ async def connection_log(self, data):
158
+ print(
159
+ f"{datetime.now()}: HANDLER|{data.get('handler')} -> {data.get('message')}. STATUS: {data.get('status')}"
160
+ )
@@ -0,0 +1,27 @@
1
+ from pydantic import BaseModel, field_validator
2
+
3
+
4
+
5
+ class AppConfiguration(BaseModel):
6
+ handler:str
7
+ name: str | None = None
8
+ image: str | None = None
9
+ introduction: str| None = None
10
+ suggestions: list[str] | None = None
11
+
12
+ @field_validator("suggestions", mode="before")
13
+ @classmethod
14
+ def process_suggestions(cls, suggestions:list[str] | None) -> list[str] | None:
15
+ if not suggestions:
16
+ return None
17
+ elif isinstance(suggestions, list):
18
+ if len(suggestions) <= 4:
19
+ return suggestions
20
+ else:
21
+ raise Exception(f"suggestions can be at max 4")
22
+
23
+
24
+ # @field_validator("image", mode="before")
25
+ # @classmethod
26
+ # def process_image(cls, image:str | None) -> str:
27
+ # return image
@@ -0,0 +1,3 @@
1
+ HANDLER_PATTERN = "[a-zA-Z][a-zA-Z0-9_-]{0,25}"
2
+ # CYCLS_URL = "https://api.cycls.com"
3
+ CYCLS_URL = "http://0.0.0.0:8004"
@@ -0,0 +1,119 @@
1
+ from enum import Enum, auto
2
+ from pydantic import BaseModel, field_validator
3
+ from typing import Any, TypeVar
4
+ from cycls import UI
5
+ from datetime import datetime
6
+ from socketio import AsyncClient
7
+ import uuid
8
+ from contextlib import asynccontextmanager
9
+
10
+ MessageContent = TypeVar('MessageContent',bound=UI.Text| UI.Image| None)
11
+ class InputTypeHint(Enum):
12
+ EMPTY = auto()
13
+ MESSAGE = auto()
14
+ CONVERSATION_ID = auto()
15
+ USER = auto()
16
+ SESSION = auto()
17
+ FULL = auto()
18
+ MESSAGE_CONTENT = auto()
19
+
20
+ class MessageRole(Enum):
21
+ ASSISTANT = "ASSISTANT"
22
+ USER = "user"
23
+ CYCLS = "cycls"
24
+
25
+ class Message(BaseModel):
26
+ id: str
27
+ created_at: datetime
28
+ content: MessageContent
29
+ role: MessageRole = MessageRole.ASSISTANT
30
+ meta: dict[str, Any] | None = None
31
+
32
+
33
+ @field_validator("content", mode="before")
34
+ @classmethod
35
+ def create_content(cls, values: dict[str, Any]) :
36
+ content_type = values.get("type")
37
+ if content_type == "text":
38
+ return UI.Text(**values)
39
+ elif content_type == "image":
40
+ return UI.Image(**values)
41
+ else:
42
+ raise ValueError(f"Unknown content type: {content_type}")
43
+
44
+
45
+ Meta = dict[str, Any]
46
+ ConversationID = str
47
+
48
+ class ConversationSession(BaseModel):
49
+ id : str
50
+
51
+ async def get_history(self):
52
+ return
53
+
54
+
55
+
56
+
57
+
58
+ class Response(BaseModel):
59
+ messages: list[UI.Text| UI.Image]
60
+ meta: dict[str, Any] | None = None
61
+
62
+ class send_base:
63
+ def __init__(self, sio:AsyncClient, user_message_id) -> None:
64
+ self.sio = sio
65
+ self.user_message_id = user_message_id
66
+ async def send_message(self, content, id, stream:bool=False, finish_reason:str|None=None):
67
+ if isinstance(content, BaseModel):
68
+ content = content.model_dump(mode="json", exclude_none=True)
69
+ await self.sio.emit(
70
+ "response",
71
+ {
72
+ "content":content,
73
+ "id": id,
74
+ "user_message_id": self.user_message_id,
75
+ "finish_reason": finish_reason,
76
+ "stream":stream
77
+ },
78
+ )
79
+
80
+ class Send(send_base):
81
+ async def text(self, message):
82
+ id = str(uuid.uuid4())
83
+ content = UI.Text(text=message)
84
+ await self.send_message(content=content, id=id)
85
+
86
+ class SendStream(send_base):
87
+ @asynccontextmanager
88
+ async def text(self):
89
+ id = str(uuid.uuid4())
90
+
91
+ async def send(chunk):
92
+ await self.send_message(UI.Text(text=chunk), id, True)
93
+
94
+ try:
95
+ yield send
96
+ except:
97
+ await self.send_message(None, id, True, "error")
98
+ finally:
99
+ pass
100
+
101
+ class userMessage(BaseModel):
102
+ message: Message
103
+ session : ConversationSession
104
+ meta: dict[str, Any] | None = None
105
+
106
+
107
+ class UserMessage:
108
+ message: Message
109
+ session : ConversationSession
110
+ meta: dict[str, Any] | None = None
111
+ send:Send
112
+ stream:SendStream
113
+ def __init__(self, message, session, meta, sio):
114
+ m = userMessage(message=message, session=session, meta=meta)
115
+ self.message = m.message
116
+ self.session = m.session
117
+ self.meta = m.meta
118
+ self.send = Send(sio=sio, user_message_id=self.message.id)
119
+ self.stream = SendStream(sio=sio, user_message_id=self.message.id)
@@ -0,0 +1,25 @@
1
+ [tool.poetry]
2
+ name = "cycls"
3
+ version = "0.0.1.0"
4
+ description = "Publish your chat app with cycls"
5
+ authors = ["Khalid Alrasheed <khalid@cycls.io>"]
6
+ packages = [{ include = "cycls" }]
7
+
8
+ [tool.poetry.dependencies]
9
+ python = "^3.8"
10
+ asyncio = "^3.4.3"
11
+ certifi = "^2023.11.17"
12
+ httpx = "^0.25.2"
13
+ pydantic = "^2.5.2"
14
+ python-socketio = { extras = ["asyncio-client"], version = "^5.10.0" }
15
+
16
+ [tool.poetry.group.dev.dependencies]
17
+ black = { version = "^23.11.0", allow-prereleases = true }
18
+
19
+ [build-system]
20
+ requires = ["poetry-core"]
21
+ build-backend = "poetry.core.masonry.api"
22
+
23
+ [tool.pyright]
24
+ venvPath = "."
25
+ venv = ".venv"