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.
- dandy/__init__.py +1 -0
- dandy/__pycache__/__init__.cpython-311.pyc +0 -0
- dandy/bot/__init__.py +2 -0
- dandy/bot/bot.py +8 -0
- dandy/bot/exceptions.py +2 -0
- dandy/bot/llm_bot.py +30 -0
- dandy/contrib/__init__.py +0 -0
- dandy/contrib/bots/__init__.py +1 -0
- dandy/contrib/bots/choice_llm_bot.py +148 -0
- dandy/core/__init__.py +0 -0
- dandy/core/exceptions.py +2 -0
- dandy/core/singleton.py +7 -0
- dandy/core/type_vars.py +6 -0
- dandy/core/url.py +38 -0
- dandy/handler/__init__.py +0 -0
- dandy/handler/handler.py +9 -0
- dandy/llm/__init__.py +0 -0
- dandy/llm/config.py +95 -0
- dandy/llm/exceptions.py +12 -0
- dandy/llm/prompt/__init__.py +2 -0
- dandy/llm/prompt/prompt.py +208 -0
- dandy/llm/prompt/snippet.py +125 -0
- dandy/llm/prompt/tests/__init__.py +0 -0
- dandy/llm/prompt/tests/test_prompt.py +18 -0
- dandy/llm/service/__init__.py +1 -0
- dandy/llm/service/messages.py +6 -0
- dandy/llm/service/prompts.py +48 -0
- dandy/llm/service/request.py +18 -0
- dandy/llm/service/service.py +123 -0
- dandy/llm/service/tests/__init__.py +0 -0
- dandy/llm/service/tests/test_service.py +26 -0
- dandy/llm/tests/__init__.py +0 -0
- dandy/llm/tests/configs.py +18 -0
- dandy/llm/tests/models.py +18 -0
- dandy/llm/tests/prompts.py +8 -0
- dandy/llm/utils.py +42 -0
- dandy/workflow/__init__.py +0 -0
- dandy/workflow/exceptions.py +5 -0
- dandy/workflow/workflow.py +9 -0
- dandy-0.0.3.dist-info/LICENSE.md +22 -0
- dandy-0.0.3.dist-info/METADATA +22 -0
- dandy-0.0.3.dist-info/RECORD +54 -0
- dandy-0.0.3.dist-info/WHEEL +5 -0
- dandy-0.0.3.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/bots/__init__.py +0 -0
- tests/bots/existing_work_orders_bot.py +10 -0
- tests/bots/work_order_comparison_bot.py +33 -0
- tests/factories.py +41 -0
- tests/models/__init__.py +0 -0
- tests/models/work_order_models.py +13 -0
- tests/test_dandy.py +12 -0
- tests/workflows/__init__.py +0 -0
- tests/workflows/find_similar_work_orders_workflows.py +6 -0
dandy/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.3"
|
|
Binary file
|
dandy/bot/__init__.py
ADDED
dandy/bot/bot.py
ADDED
dandy/bot/exceptions.py
ADDED
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
|
dandy/core/exceptions.py
ADDED
dandy/core/singleton.py
ADDED
dandy/core/type_vars.py
ADDED
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
|
dandy/handler/handler.py
ADDED
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
|
+
|
dandy/llm/exceptions.py
ADDED
|
@@ -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,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
|