dandy 0.0.3__py3-none-any.whl

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 (54) hide show
  1. dandy/__init__.py +1 -0
  2. dandy/__pycache__/__init__.cpython-311.pyc +0 -0
  3. dandy/bot/__init__.py +2 -0
  4. dandy/bot/bot.py +8 -0
  5. dandy/bot/exceptions.py +2 -0
  6. dandy/bot/llm_bot.py +30 -0
  7. dandy/contrib/__init__.py +0 -0
  8. dandy/contrib/bots/__init__.py +1 -0
  9. dandy/contrib/bots/choice_llm_bot.py +148 -0
  10. dandy/core/__init__.py +0 -0
  11. dandy/core/exceptions.py +2 -0
  12. dandy/core/singleton.py +7 -0
  13. dandy/core/type_vars.py +6 -0
  14. dandy/core/url.py +38 -0
  15. dandy/handler/__init__.py +0 -0
  16. dandy/handler/handler.py +9 -0
  17. dandy/llm/__init__.py +0 -0
  18. dandy/llm/config.py +95 -0
  19. dandy/llm/exceptions.py +12 -0
  20. dandy/llm/prompt/__init__.py +2 -0
  21. dandy/llm/prompt/prompt.py +208 -0
  22. dandy/llm/prompt/snippet.py +125 -0
  23. dandy/llm/prompt/tests/__init__.py +0 -0
  24. dandy/llm/prompt/tests/test_prompt.py +18 -0
  25. dandy/llm/service/__init__.py +1 -0
  26. dandy/llm/service/messages.py +6 -0
  27. dandy/llm/service/prompts.py +48 -0
  28. dandy/llm/service/request.py +18 -0
  29. dandy/llm/service/service.py +123 -0
  30. dandy/llm/service/tests/__init__.py +0 -0
  31. dandy/llm/service/tests/test_service.py +26 -0
  32. dandy/llm/tests/__init__.py +0 -0
  33. dandy/llm/tests/configs.py +18 -0
  34. dandy/llm/tests/models.py +18 -0
  35. dandy/llm/tests/prompts.py +8 -0
  36. dandy/llm/utils.py +42 -0
  37. dandy/workflow/__init__.py +0 -0
  38. dandy/workflow/exceptions.py +5 -0
  39. dandy/workflow/workflow.py +9 -0
  40. dandy-0.0.3.dist-info/LICENSE.md +22 -0
  41. dandy-0.0.3.dist-info/METADATA +22 -0
  42. dandy-0.0.3.dist-info/RECORD +54 -0
  43. dandy-0.0.3.dist-info/WHEEL +5 -0
  44. dandy-0.0.3.dist-info/top_level.txt +2 -0
  45. tests/__init__.py +0 -0
  46. tests/bots/__init__.py +0 -0
  47. tests/bots/existing_work_orders_bot.py +10 -0
  48. tests/bots/work_order_comparison_bot.py +33 -0
  49. tests/factories.py +41 -0
  50. tests/models/__init__.py +0 -0
  51. tests/models/work_order_models.py +13 -0
  52. tests/test_dandy.py +12 -0
  53. tests/workflows/__init__.py +0 -0
  54. tests/workflows/find_similar_work_orders_workflows.py +6 -0
dandy/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.3"
dandy/bot/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ from .bot import Bot
2
+ from .llm_bot import LlmBot
dandy/bot/bot.py ADDED
@@ -0,0 +1,8 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+ from dandy.handler.handler import Handler
5
+
6
+
7
+ class Bot(Handler, ABC):
8
+ ...
@@ -0,0 +1,2 @@
1
+ class BotException(Exception):
2
+ pass
dandy/bot/llm_bot.py ADDED
@@ -0,0 +1,30 @@
1
+ from abc import ABC
2
+ from typing import Type
3
+
4
+ from dandy.bot.bot import Bot
5
+ from dandy.core.type_vars import ModelType
6
+ from dandy.llm.config import LlmConfig
7
+ from dandy.llm.prompt import Prompt
8
+
9
+
10
+ class LlmBot(Bot, ABC):
11
+ role_prompt: Prompt
12
+ instructions_prompt: Prompt
13
+ llm_config: LlmConfig
14
+
15
+ @classmethod
16
+ def process_prompt_to_model_object(
17
+ cls,
18
+ prompt: Prompt,
19
+ model: Type[ModelType],
20
+ ) -> ModelType:
21
+
22
+ return cls.llm_config.service.process_prompt_to_model_object(
23
+ prompt=prompt,
24
+ model=model,
25
+ prefix_system_prompt=(
26
+ Prompt()
27
+ .prompt(cls.role_prompt)
28
+ .prompt(cls.instructions_prompt)
29
+ )
30
+ )
File without changes
@@ -0,0 +1 @@
1
+ from dandy.contrib.bots.choice_llm_bot import SingleChoiceLlmBot, MultipleChoiceLlmBot
@@ -0,0 +1,148 @@
1
+ from abc import ABC
2
+ from enum import Enum
3
+ from typing import Tuple, List, Union, overload, Type
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from dandy.bot import LlmBot
8
+ from dandy.bot.exceptions import BotException
9
+ from dandy.llm.prompt import Prompt
10
+
11
+
12
+ NO_CHOICE_FOUND_RESPONSE = 'no-choice-match-found'
13
+
14
+
15
+ class SingleChoiceResponse(BaseModel):
16
+ selected_choice: str
17
+
18
+
19
+ class MultipleChoiceResponse(BaseModel):
20
+ selected_choices: List[str]
21
+
22
+
23
+ class _ChoiceLlmBot(LlmBot, ABC):
24
+ role_prompt = (
25
+ Prompt()
26
+ .text('You are an choice bot.')
27
+ )
28
+
29
+ @classmethod
30
+ def process(
31
+ cls,
32
+ user_input: str,
33
+ choices: Union[Type[Enum], List[str], Tuple[str]],
34
+ choice_response_model: Union[Type[SingleChoiceResponse], Type[MultipleChoiceResponse]]
35
+ ) -> Union[SingleChoiceResponse, MultipleChoiceResponse]:
36
+
37
+ prompt = (
38
+ Prompt()
39
+ .text('This is the user input:')
40
+ .text(user_input, triple_quote=True)
41
+ .text('These are the choices:')
42
+ )
43
+
44
+ if isinstance(choices, type) and issubclass(choices, Enum):
45
+ prompt.unordered_random_list([choice.value for choice in choices], triple_quote=True)
46
+ elif isinstance(choices, (list, tuple)):
47
+ prompt.unordered_random_list(choices, triple_quote=True)
48
+ else:
49
+ raise BotException('Choices must be an Enum, a list or a tuple.')
50
+
51
+ return cls.process_prompt_to_model_object(
52
+ prompt=prompt,
53
+ model=choice_response_model
54
+ )
55
+
56
+
57
+ class _ChoiceOverloadMixin:
58
+ @classmethod
59
+ @overload
60
+ def process(
61
+ cls,
62
+ user_input: str,
63
+ choices: Union[List[str], Tuple[str]],
64
+ choice_response_model: Type[BaseModel]
65
+ ) -> Union[str, List[str], None]:
66
+ ...
67
+
68
+ @classmethod
69
+ @overload
70
+ def process(
71
+ cls,
72
+ user_input: str,
73
+ choices: Type[Enum],
74
+ choice_response_model: Type[BaseModel]
75
+ ) -> Union[Enum, List[Enum], None]:
76
+ ...
77
+
78
+ @classmethod
79
+ def process(
80
+ cls,
81
+ user_input: str,
82
+ choices: Union[Type[Enum], List[str], Tuple[str]],
83
+ **kwargs
84
+ ) -> Union[Enum, List[Enum], str, List[str], None]:
85
+ ...
86
+
87
+
88
+ class SingleChoiceLlmBot(_ChoiceLlmBot, _ChoiceOverloadMixin):
89
+ instructions_prompt = (
90
+ Prompt()
91
+ .text('Your job is to identify the intent of the user input and match it to the provided choices.')
92
+ .text(f'If there is no good matches in the choices reply with value "{NO_CHOICE_FOUND_RESPONSE}".')
93
+ )
94
+
95
+ @classmethod
96
+ def process(
97
+ cls,
98
+ user_input: str,
99
+ choices: Union[Type[Enum], List[str], Tuple[str]],
100
+ **kwargs
101
+ ) -> Union[Enum, str, None]:
102
+
103
+ choice_response = super().process(
104
+ user_input=user_input,
105
+ choices=choices,
106
+ choice_response_model=SingleChoiceResponse,
107
+ )
108
+
109
+ selected_choice = choice_response.selected_choice
110
+ if selected_choice == NO_CHOICE_FOUND_RESPONSE:
111
+ return None
112
+ else:
113
+ if isinstance(choices, type) and issubclass(choices, Enum):
114
+ return choices(selected_choice)
115
+ else:
116
+ return selected_choice
117
+
118
+
119
+ class MultipleChoiceLlmBot(_ChoiceLlmBot, _ChoiceOverloadMixin):
120
+ instructions_prompt = (
121
+ Prompt()
122
+ .text('Your job is to identify the intent of the user input and match it to the provided choices.')
123
+ .text('Return as many choices as you see relevant to the user input.')
124
+ .text(f'If there is no good matches in the choices reply with value "{NO_CHOICE_FOUND_RESPONSE}".')
125
+ )
126
+
127
+ @classmethod
128
+ def process(
129
+ cls,
130
+ user_input: str,
131
+ choices: Union[Type[Enum], List[str], Tuple[str]],
132
+ **kwargs
133
+ ) -> Union[List[Enum], List[str], None]:
134
+
135
+ choice_response = super().process(
136
+ user_input=user_input,
137
+ choices=choices,
138
+ choice_response_model=MultipleChoiceResponse
139
+ )
140
+
141
+ select_choices = choice_response.selected_choices
142
+ if NO_CHOICE_FOUND_RESPONSE in select_choices:
143
+ return None
144
+ else:
145
+ if isinstance(choices, type) and issubclass(choices, Enum):
146
+ return [choices(choice) for choice in select_choices]
147
+ else:
148
+ return select_choices
dandy/core/__init__.py ADDED
File without changes
@@ -0,0 +1,2 @@
1
+ class DandyException(Exception):
2
+ pass
@@ -0,0 +1,7 @@
1
+ class Singleton:
2
+ _instance = None
3
+
4
+ def __new__(cls, *args, **kwargs):
5
+ if cls._instance is None:
6
+ cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
7
+ return cls._instance
@@ -0,0 +1,6 @@
1
+ from typing import TypeVar
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ ModelType = TypeVar('ModelType', bound=BaseModel)
dandy/core/url.py ADDED
@@ -0,0 +1,38 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Dict
3
+ from urllib.parse import urlencode, urlparse, ParseResult, quote
4
+
5
+
6
+ @dataclass(kw_only=True)
7
+ class Url:
8
+ host: str
9
+ path_parameters: List[str] = list
10
+ query_parameters: Dict[str, str] = dict
11
+
12
+
13
+ @property
14
+ def parsed_url(self) -> ParseResult:
15
+ return urlparse(self.host)
16
+
17
+ @property
18
+ def path(self) -> str:
19
+ return self.path_parameters_to_str + self.query_parameters_to_str
20
+
21
+ @property
22
+ def is_https(self) -> bool:
23
+ return self.parsed_url.scheme == 'https'
24
+
25
+ @property
26
+ def path_parameters_to_str(self) -> str:
27
+ if self.path_parameters:
28
+ return '/' + '/'.join([quote(parameter) for parameter in self.path_parameters])
29
+
30
+ return ''
31
+
32
+ @property
33
+ def query_parameters_to_str(self) -> str:
34
+ if self.query_parameters:
35
+ query = urlencode(self.query_parameters)
36
+ return '?' + query
37
+
38
+ return ''
File without changes
@@ -0,0 +1,9 @@
1
+ from abc import abstractmethod, ABC
2
+ from typing import Any
3
+
4
+
5
+ class Handler(ABC):
6
+ @classmethod
7
+ @abstractmethod
8
+ def process(cls, **kwargs: Any) -> Any:
9
+ ...
dandy/llm/__init__.py ADDED
File without changes
dandy/llm/config.py ADDED
@@ -0,0 +1,95 @@
1
+ from abc import abstractmethod
2
+ from typing import Optional, List
3
+
4
+ from dandy.core.url import Url
5
+ from dandy.llm.service import Service
6
+
7
+
8
+ class LlmConfig:
9
+ def __init__(
10
+ self,
11
+ host: str,
12
+ port: int,
13
+ model: str,
14
+ path_parameters: Optional[List[str]] = None,
15
+ query_parameters: Optional[dict] = None,
16
+ headers: Optional[dict] = None,
17
+ api_key: Optional[str] = None,
18
+ retry_count: int = 10,
19
+ ):
20
+ if headers is None:
21
+ headers = {
22
+ "Content-Type": "application/json",
23
+ "Accept": "application/json",
24
+ }
25
+
26
+ if api_key is not None:
27
+ headers["Authorization"] = f"Bearer {api_key}"
28
+
29
+ self.url=Url(
30
+ host=host,
31
+ path_parameters=path_parameters,
32
+ query_parameters=query_parameters,
33
+ )
34
+ self.port=port
35
+ self.model=model
36
+ self.headers=headers
37
+
38
+ self.retry_count = retry_count
39
+
40
+ @property
41
+ def service(self):
42
+ return Service(self)
43
+
44
+ @abstractmethod
45
+ def get_response_content(self, response) -> str:
46
+ ...
47
+
48
+
49
+ class OllamaLlmConfig(LlmConfig):
50
+ def __init__(
51
+ self,
52
+ host: str,
53
+ port: int,
54
+ model: str,
55
+ api_key: Optional[str] = None,
56
+ ):
57
+ super().__init__(
58
+ host=host,
59
+ port=port,
60
+ model=model,
61
+ path_parameters=[
62
+ 'api',
63
+ 'chat',
64
+ ],
65
+ api_key=api_key,
66
+ )
67
+
68
+ def get_response_content(self, response) -> str:
69
+ return response['message']['content']
70
+
71
+
72
+ class OpenaiLlmConfig(LlmConfig):
73
+ def __init__(
74
+ self,
75
+ host: str,
76
+ port: int,
77
+ model: str,
78
+ api_key: Optional[str] = None,
79
+ ):
80
+ super().__init__(
81
+ host=host,
82
+ port=port,
83
+ model=model,
84
+ path_parameters=[
85
+ 'v1',
86
+ 'chat',
87
+ 'completions',
88
+ ],
89
+ api_key=api_key,
90
+ )
91
+
92
+ def get_response_content(self, response) -> str:
93
+ return response['choices'][0]['message']['content']
94
+
95
+
@@ -0,0 +1,12 @@
1
+ from typing import List
2
+
3
+ from dandy.core.exceptions import DandyException
4
+
5
+
6
+ class LlmException(DandyException):
7
+ pass
8
+
9
+
10
+ class LlmValidationException(LlmException):
11
+ def __init__(self, name: str, choices: List[str]):
12
+ super().__init__(f'Did not get a valid response format from llm service')
@@ -0,0 +1,2 @@
1
+ from dandy.llm.prompt.prompt import Prompt
2
+ from dandy.llm.prompt.snippet import Snippet
@@ -0,0 +1,208 @@
1
+ from typing import List, Type, Self, Dict
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from dandy.llm.prompt import snippet
6
+
7
+
8
+ CHARACTERS_PER_TOKEN = 4
9
+
10
+
11
+ class Prompt:
12
+ def __init__(
13
+ self,
14
+ tag: str = None
15
+ ):
16
+ self.snippets: List[snippet.Snippet] = []
17
+ self.tag = tag
18
+
19
+ def __str__(self) -> str:
20
+ return self.to_str()
21
+
22
+ def to_str(self) -> str:
23
+ prompt_string = ''.join([_.to_str() for _ in self.snippets])
24
+
25
+ if isinstance(self.tag, str):
26
+ return f'<{self.tag}>\n{prompt_string}\n</{self.tag}>\n'
27
+ else:
28
+ return prompt_string
29
+
30
+ def dict(
31
+ self,
32
+ dictionary: Dict,
33
+ triple_quote: bool = False
34
+ ) -> Self:
35
+
36
+ self.snippets.append(
37
+ snippet.DictionarySnippet(
38
+ dictionary=dictionary,
39
+ triple_quote=triple_quote
40
+ )
41
+ )
42
+
43
+ return self
44
+
45
+ def divider(self) -> Self:
46
+ self.snippets.append(snippet.DividerSnippet())
47
+
48
+ return self
49
+
50
+ @property
51
+ def estimated_token_count(self) -> int:
52
+ return int(len(self.to_str()) / CHARACTERS_PER_TOKEN)
53
+
54
+
55
+ def title(
56
+ self,
57
+ title: str,
58
+ triple_quote: bool = False
59
+ ) -> Self:
60
+
61
+ self.snippets.append(
62
+ snippet.TitleSnippet(
63
+ title=title,
64
+ triple_quote=triple_quote
65
+ )
66
+ )
67
+
68
+ return self
69
+
70
+ def line_break(self) -> Self:
71
+ self.snippets.append(snippet.LineBreakSnippet())
72
+
73
+ return self
74
+
75
+ def list(
76
+ self,
77
+ items: List[str],
78
+ triple_quote: bool = False
79
+ ) -> Self:
80
+
81
+ self.unordered_list(
82
+ items=items,
83
+ triple_quote=triple_quote
84
+ )
85
+
86
+ return self
87
+
88
+ def model_object(
89
+ self,
90
+ model_object: BaseModel,
91
+ triple_quote: bool = False
92
+ ) -> Self:
93
+
94
+ self.snippets.append(
95
+ snippet.ModelObject(
96
+ model_object=model_object,
97
+ triple_quote=triple_quote
98
+ )
99
+ )
100
+
101
+ return self
102
+
103
+ def model_schema(
104
+ self,
105
+ model: Type[BaseModel],
106
+ triple_quote: bool = False
107
+ ) -> Self:
108
+
109
+ self.snippets.append(
110
+ snippet.ModelSchema(
111
+ model=model,
112
+ triple_quote=triple_quote
113
+ )
114
+ )
115
+
116
+ return self
117
+
118
+ def ordered_list(
119
+ self,
120
+ items: List[str],
121
+ triple_quote: bool = False
122
+ ) -> Self:
123
+
124
+ self.snippets.append(
125
+ snippet.OrderedListSnippet(
126
+ items=items,
127
+ triple_quote=triple_quote
128
+ )
129
+ )
130
+
131
+ return self
132
+
133
+ def prompt(
134
+ self,
135
+ prompt: Self,
136
+ triple_quote: bool = False
137
+ ) -> Self:
138
+
139
+ self.snippets.append(
140
+ snippet.PromptSnippet(
141
+ prompt=prompt,
142
+ triple_quote=triple_quote
143
+ )
144
+ )
145
+
146
+ return self
147
+
148
+ def random_choice(
149
+ self,
150
+ choices: List[str],
151
+ triple_quote: bool = False
152
+ ) -> Self:
153
+
154
+ self.snippets.append(
155
+ snippet.RandomChoiceSnippet(
156
+ choices=choices,
157
+ triple_quote=triple_quote,
158
+ )
159
+ )
160
+
161
+ return self
162
+
163
+ def text(
164
+ self,
165
+ text: str,
166
+ label: str = '',
167
+ triple_quote: bool = False
168
+ ) -> Self:
169
+
170
+ self.snippets.append(
171
+ snippet.TextSnippet(
172
+ text=text,
173
+ label=label,
174
+ triple_quote=triple_quote
175
+ )
176
+ )
177
+
178
+ return self
179
+
180
+ def unordered_list(
181
+ self,
182
+ items: List[str],
183
+ triple_quote: bool = False
184
+ ) -> Self:
185
+
186
+ self.snippets.append(
187
+ snippet.UnorderedListSnippet(
188
+ items=items,
189
+ triple_quote=triple_quote
190
+ )
191
+ )
192
+
193
+ return self
194
+
195
+ def unordered_random_list(
196
+ self,
197
+ items: List[str],
198
+ triple_quote: bool = False
199
+ ) -> Self:
200
+
201
+ self.snippets.append(
202
+ snippet.UnorderedRandomListSnippet(
203
+ items=items,
204
+ triple_quote=triple_quote
205
+ )
206
+ )
207
+
208
+ return self