writer 0.6.0__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.
- writer/__init__.py +130 -0
- writer/ai.py +480 -0
- writer/app_runner.py +834 -0
- writer/app_templates/ai-starter/README.md +3 -0
- writer/app_templates/ai-starter/main.py +14 -0
- writer/app_templates/ai-starter/pyproject.toml +15 -0
- writer/app_templates/ai-starter/static/README.md +8 -0
- writer/app_templates/ai-starter/static/favicon.png +0 -0
- writer/app_templates/ai-starter/ui.json +54 -0
- writer/app_templates/default/README.md +3 -0
- writer/app_templates/default/main.py +40 -0
- writer/app_templates/default/pyproject.toml +15 -0
- writer/app_templates/default/static/README.md +8 -0
- writer/app_templates/default/static/favicon.png +0 -0
- writer/app_templates/default/ui.json +158 -0
- writer/app_templates/hello/README.md +3 -0
- writer/app_templates/hello/assets/main_df.csv +21 -0
- writer/app_templates/hello/assets/story.txt +17 -0
- writer/app_templates/hello/main.py +151 -0
- writer/app_templates/hello/pyproject.toml +15 -0
- writer/app_templates/hello/static/README.md +13 -0
- writer/app_templates/hello/static/favicon.png +0 -0
- writer/app_templates/hello/static/pigeon1.jpg +0 -0
- writer/app_templates/hello/test_app.py +44 -0
- writer/app_templates/hello/ui.json +1068 -0
- writer/app_templates/pdg-tutorial/README.md +3 -0
- writer/app_templates/pdg-tutorial/main.py +17 -0
- writer/app_templates/pdg-tutorial/prompts.py +66 -0
- writer/app_templates/pdg-tutorial/pyproject.toml +15 -0
- writer/app_templates/pdg-tutorial/static/README.md +8 -0
- writer/app_templates/pdg-tutorial/static/favicon.png +0 -0
- writer/app_templates/pdg-tutorial/ui.json +169 -0
- writer/app_templates/quickstart/README.md +3 -0
- writer/app_templates/quickstart/main.py +113 -0
- writer/app_templates/quickstart/pyproject.toml +15 -0
- writer/app_templates/quickstart/requirements.txt +1 -0
- writer/app_templates/quickstart/static/README.md +13 -0
- writer/app_templates/quickstart/static/favicon.png +0 -0
- writer/app_templates/quickstart/ui.json +192 -0
- writer/app_templates/text-demo/README.md +3 -0
- writer/app_templates/text-demo/main.py +29 -0
- writer/app_templates/text-demo/pyproject.toml +15 -0
- writer/app_templates/text-demo/static/README.md +8 -0
- writer/app_templates/text-demo/static/favicon.png +0 -0
- writer/app_templates/text-demo/ui.json +149 -0
- writer/auth.py +245 -0
- writer/command_line.py +164 -0
- writer/core.py +1503 -0
- writer/core_ui.py +433 -0
- writer/deploy.py +186 -0
- writer/mypy.ini +2 -0
- writer/py.typed +0 -0
- writer/serve.py +596 -0
- writer/ss_types.py +163 -0
- writer/static/assets/Arrow.dom-C52_vrWm.js +1 -0
- writer/static/assets/BuilderApp-DakBH-QB.js +743 -0
- writer/static/assets/BuilderApp-DjkgRMd3.css +1 -0
- writer/static/assets/ComponentRenderer-BnXm57ig.css +1 -0
- writer/static/assets/ComponentRenderer-PYR7CN9R.js +1 -0
- writer/static/assets/abap-D8nrxEjS.js +6 -0
- writer/static/assets/apex-BrXDlLUW.js +6 -0
- writer/static/assets/azcli-CElzELwZ.js +6 -0
- writer/static/assets/bat-CUsyEhik.js +6 -0
- writer/static/assets/bicep-BtxyJn6H.js +7 -0
- writer/static/assets/cameligo-ClBCoF8h.js +6 -0
- writer/static/assets/clojure-B9TqLHAk.js +6 -0
- writer/static/assets/codicon-BA2IlpFX.ttf +0 -0
- writer/static/assets/coffee-DYsfeylR.js +6 -0
- writer/static/assets/cpp-VVGvvgir.js +6 -0
- writer/static/assets/csharp-Z6z2stHy.js +6 -0
- writer/static/assets/csp-DgZoLDI1.js +6 -0
- writer/static/assets/css-KqQ96-gC.js +8 -0
- writer/static/assets/css.worker-DvNUQFd1.js +84 -0
- writer/static/assets/cssMode-BjK5GHby.js +9 -0
- writer/static/assets/cypher-CYoSlgTu.js +6 -0
- writer/static/assets/dart-BGDl7St1.js +6 -0
- writer/static/assets/dockerfile-CuCtxA7T.js +6 -0
- writer/static/assets/ecl-BCTFAUpS.js +6 -0
- writer/static/assets/editor.worker-BVwmgLrR.js +11 -0
- writer/static/assets/elixir-C7hRTYZ9.js +6 -0
- writer/static/assets/flow9-Bi_qi707.js +6 -0
- writer/static/assets/freemarker2-i8lRH5rw.js +8 -0
- writer/static/assets/fsharp-CxaaEKKi.js +6 -0
- writer/static/assets/go-DUImKuGY.js +6 -0
- writer/static/assets/graphql-D5sGVkLV.js +6 -0
- writer/static/assets/handlebars-C-M0xH-3.js +6 -0
- writer/static/assets/hcl-zD_CCkZ1.js +6 -0
- writer/static/assets/html-D4h1E1bc.js +6 -0
- writer/static/assets/html.worker-BJMlcbMU.js +458 -0
- writer/static/assets/htmlMode-B2YRTiG6.js +9 -0
- writer/static/assets/index-BaC5au8C.js +8 -0
- writer/static/assets/index-DMFOqlrb.js +1 -0
- writer/static/assets/index-DaNZqg5r.js +16 -0
- writer/static/assets/index-XEiBUN1q.css +1 -0
- writer/static/assets/index-cJO61pFl.js +367 -0
- writer/static/assets/ini-8kKHd4ZL.js +6 -0
- writer/static/assets/java-De1axCfe.js +6 -0
- writer/static/assets/javascript-BYM0Yx4u.js +6 -0
- writer/static/assets/json.worker-BwvX8PuZ.js +42 -0
- writer/static/assets/jsonMode-B8RZLFyl.js +11 -0
- writer/static/assets/julia-D3ApGBxz.js +6 -0
- writer/static/assets/kotlin-GbSrCElU.js +6 -0
- writer/static/assets/less-DNUaDNdz.js +7 -0
- writer/static/assets/lexon-Bg9QKxBu.js +6 -0
- writer/static/assets/liquid-ktIne1h-.js +6 -0
- writer/static/assets/lua-Crkvc3mc.js +6 -0
- writer/static/assets/m3-DsrzVyM1.js +6 -0
- writer/static/assets/mapbox-gl-FST67qaq.js +2026 -0
- writer/static/assets/markdown-CY5IOZuu.js +6 -0
- writer/static/assets/marked.esm-273vDTCT.js +45 -0
- writer/static/assets/mdx-Bi1uxJs7.js +6 -0
- writer/static/assets/mips-BE8RsGBA.js +6 -0
- writer/static/assets/msdax-N5ajIiFQ.js +6 -0
- writer/static/assets/mysql-DRxbB97D.js +6 -0
- writer/static/assets/objective-c-BHUZy23s.js +6 -0
- writer/static/assets/pascal-BemVzBTY.js +6 -0
- writer/static/assets/pascaligo-BACCcnx_.js +6 -0
- writer/static/assets/perl-CuU66Ptk.js +6 -0
- writer/static/assets/pgsql-CQ6TMH2r.js +6 -0
- writer/static/assets/php-BvyzZa65.js +6 -0
- writer/static/assets/pla-DrIuu9u1.js +6 -0
- writer/static/assets/plotly.min-Bn62pPWj.js +3573 -0
- writer/static/assets/postiats-BR_hrfni.js +6 -0
- writer/static/assets/powerquery-CKDUeRmd.js +6 -0
- writer/static/assets/powershell-Dsa4rhA_.js +6 -0
- writer/static/assets/protobuf-CGsvhooB.js +7 -0
- writer/static/assets/pug-D2p3uOX2.js +6 -0
- writer/static/assets/python-DGmW9UMq.js +6 -0
- writer/static/assets/qsharp-B7F3HtPF.js +6 -0
- writer/static/assets/r-3aLoi2fs.js +6 -0
- writer/static/assets/razor-NRqESbyQ.js +6 -0
- writer/static/assets/redis-jqFeRM5s.js +6 -0
- writer/static/assets/redshift-BriwQgXR.js +6 -0
- writer/static/assets/remixicon-BVOYbT3K.woff2 +0 -0
- writer/static/assets/remixicon-D9ZyeRwQ.ttf +0 -0
- writer/static/assets/remixicon-DUK49BtM.eot +0 -0
- writer/static/assets/remixicon-DfzPQSMi.woff +0 -0
- writer/static/assets/remixicon-ncU_JTfY.svg +8572 -0
- writer/static/assets/restructuredtext-hbBFZ0w9.js +6 -0
- writer/static/assets/ruby-ByThyB2Q.js +6 -0
- writer/static/assets/rust-DIEZMp5R.js +6 -0
- writer/static/assets/sb-C6Gjjw_x.js +6 -0
- writer/static/assets/scala-DZNw3jJB.js +6 -0
- writer/static/assets/scheme-55eqh71t.js +6 -0
- writer/static/assets/scss-D-OVkc4F.js +8 -0
- writer/static/assets/serialization-DDqLB4lR.js +20 -0
- writer/static/assets/shell-DSpi8_qN.js +6 -0
- writer/static/assets/solidity-BHddiNFS.js +6 -0
- writer/static/assets/sophia-D6taVZFb.js +6 -0
- writer/static/assets/sparql-LA0C7mUc.js +6 -0
- writer/static/assets/sql-C3-3IcFM.js +6 -0
- writer/static/assets/st-C4g7059C.js +6 -0
- writer/static/assets/swift-DNI1vH3h.js +8 -0
- writer/static/assets/systemverilog-DL_FVbcQ.js +6 -0
- writer/static/assets/tcl-DVJXmIwd.js +6 -0
- writer/static/assets/ts.worker-CwG1rUES.js +37021 -0
- writer/static/assets/tsMode-CjGd3dCD.js +16 -0
- writer/static/assets/twig-BVWDLtw5.js +6 -0
- writer/static/assets/typescript-8g2Igz_p.js +6 -0
- writer/static/assets/vb-Btz91-7U.js +6 -0
- writer/static/assets/vega-embed.module-CxKUxoHf.js +201 -0
- writer/static/assets/wgsl-D8V_buCG.js +303 -0
- writer/static/assets/xml-Cey2x9G0.js +6 -0
- writer/static/assets/yaml-DiYkW6--.js +6 -0
- writer/static/favicon.png +0 -0
- writer/static/index.html +93 -0
- writer/templates/auth_unauthorized.html +35 -0
- writer/ui.py +1953 -0
- writer/ui_manager.py +235 -0
- writer-0.6.0.dist-info/LICENSE.txt +190 -0
- writer-0.6.0.dist-info/METADATA +111 -0
- writer-0.6.0.dist-info/RECORD +174 -0
- writer-0.6.0.dist-info/WHEEL +4 -0
- writer-0.6.0.dist-info/entry_points.txt +3 -0
writer/__init__.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
from types import ModuleType
|
|
3
|
+
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
|
|
4
|
+
|
|
5
|
+
from writer.core import (
|
|
6
|
+
BytesWrapper,
|
|
7
|
+
Config,
|
|
8
|
+
FileWrapper,
|
|
9
|
+
Readable,
|
|
10
|
+
State,
|
|
11
|
+
WriterState,
|
|
12
|
+
base_component_tree,
|
|
13
|
+
initial_state,
|
|
14
|
+
new_initial_state,
|
|
15
|
+
session_manager,
|
|
16
|
+
session_verifier,
|
|
17
|
+
)
|
|
18
|
+
from writer.ui import WriterUIManager
|
|
19
|
+
|
|
20
|
+
VERSION = importlib.metadata.version("writer")
|
|
21
|
+
|
|
22
|
+
base_component_tree
|
|
23
|
+
session_manager
|
|
24
|
+
Config
|
|
25
|
+
session_verifier
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def pack_file(file: Union[Readable, str], mime_type: Optional[str] = None):
|
|
29
|
+
"""
|
|
30
|
+
Returns a FileWrapper for the file provided, which is automatically
|
|
31
|
+
serialised to a data URL.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
return FileWrapper(file, mime_type)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def pack_bytes(raw_data, mime_type: Optional[str] = None):
|
|
38
|
+
"""
|
|
39
|
+
Returns a BytesWrapper for the bytes raw data provided, which is automatically
|
|
40
|
+
serialised to a data URL.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
return BytesWrapper(raw_data, mime_type)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
S = TypeVar('S', bound=WriterState)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def init_ui() -> WriterUIManager:
|
|
50
|
+
"""Initializes and returns an instance of WriterUIManager.
|
|
51
|
+
This manager provides methods to dynamically create and manage UI
|
|
52
|
+
components in a Writer Framework application.
|
|
53
|
+
|
|
54
|
+
The WriterUIManager allows for the creation of application-wide,
|
|
55
|
+
code-managed components during startup, ensuring that the set of components
|
|
56
|
+
is initially accessible by all sessions.
|
|
57
|
+
|
|
58
|
+
:return: An instance that serves as a bridge for programmatically
|
|
59
|
+
interacting with the frontend, facilitating dynamic UI component management.
|
|
60
|
+
:rtype: WriterUIManager
|
|
61
|
+
|
|
62
|
+
**Example**::
|
|
63
|
+
|
|
64
|
+
>>> import writer as wf
|
|
65
|
+
>>>
|
|
66
|
+
>>> with wf.init_ui() as ui:
|
|
67
|
+
>>> with ui.Page({"key": "hello"}):
|
|
68
|
+
>>> ui.Text({"text": "Hello pigeons"})
|
|
69
|
+
"""
|
|
70
|
+
return WriterUIManager()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def init_state(raw_state: Dict[str, Any], schema: Optional[Type[S]] = None) -> Union[S, WriterState]:
|
|
74
|
+
"""
|
|
75
|
+
Sets the initial state, which will be used as the starting point for
|
|
76
|
+
every session.
|
|
77
|
+
|
|
78
|
+
initial_state.user_state.state = {}
|
|
79
|
+
initial_state.user_state.ingest(state_dict)
|
|
80
|
+
return initial_state
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
>>> import writer as wf
|
|
85
|
+
|
|
86
|
+
>>> initial_state = wf.init_state({
|
|
87
|
+
>>> "counter": 0,
|
|
88
|
+
>>> }, schema=AppSchema)
|
|
89
|
+
"""
|
|
90
|
+
concrete_schema = cast(Type[S], WriterState if schema is None else schema)
|
|
91
|
+
if not issubclass(concrete_schema, WriterState):
|
|
92
|
+
raise ValueError("Root schema must inherit from WriterState")
|
|
93
|
+
|
|
94
|
+
_initial_state: S = new_initial_state(concrete_schema, raw_state)
|
|
95
|
+
return _initial_state
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def init_handlers(handler_modules: Union[List[ModuleType], ModuleType]):
|
|
99
|
+
"""
|
|
100
|
+
Registers one or more handler modules to enable its containing functions
|
|
101
|
+
to be used as event handlers by the application's frontend.
|
|
102
|
+
|
|
103
|
+
:param handler_modules: A module or list of modules for registration.
|
|
104
|
+
:type handler_modules: module or list of modules
|
|
105
|
+
|
|
106
|
+
**Examples**
|
|
107
|
+
|
|
108
|
+
Register a single handler module:
|
|
109
|
+
|
|
110
|
+
>>> import writer as wf
|
|
111
|
+
>>> import my_handler_module
|
|
112
|
+
>>> wf.init_handlers(my_handler_module)
|
|
113
|
+
|
|
114
|
+
Register multiple handler modules:
|
|
115
|
+
|
|
116
|
+
>>> import writer as wf
|
|
117
|
+
>>> import module_one, module_two
|
|
118
|
+
>>> wf.init_handlers([module_one, module_two])
|
|
119
|
+
|
|
120
|
+
:raises ValueError: If an object that is not a module is attempted to be registered.
|
|
121
|
+
"""
|
|
122
|
+
from writer.core import get_app_process
|
|
123
|
+
current_app_process = get_app_process()
|
|
124
|
+
handler_registry = current_app_process.handler_registry
|
|
125
|
+
# Ensure handler_modules is a list
|
|
126
|
+
if not isinstance(handler_modules, list):
|
|
127
|
+
handler_modules = [handler_modules]
|
|
128
|
+
|
|
129
|
+
for module in handler_modules:
|
|
130
|
+
handler_registry.register_module(module)
|
writer/ai.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Generator, Iterable, List, Literal, Optional, TypedDict, Union, cast
|
|
3
|
+
|
|
4
|
+
from httpx import Timeout
|
|
5
|
+
from writerai import Writer
|
|
6
|
+
from writerai._exceptions import WriterError
|
|
7
|
+
from writerai._streaming import Stream
|
|
8
|
+
from writerai._types import Body, Headers, NotGiven, Query
|
|
9
|
+
from writerai.types import Chat, Completion, StreamingData
|
|
10
|
+
from writerai.types.chat_chat_params import Message as WriterAIMessage
|
|
11
|
+
|
|
12
|
+
from writer.core import get_app_process
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ChatOptions(TypedDict, total=False):
|
|
16
|
+
model: str
|
|
17
|
+
max_tokens: Union[int, NotGiven]
|
|
18
|
+
n: Union[int, NotGiven]
|
|
19
|
+
stop: Union[List[str], str, NotGiven]
|
|
20
|
+
temperature: Union[float, NotGiven]
|
|
21
|
+
top_p: Union[float, NotGiven]
|
|
22
|
+
extra_headers: Optional[Headers]
|
|
23
|
+
extra_query: Optional[Query]
|
|
24
|
+
extra_body: Optional[Body]
|
|
25
|
+
timeout: Union[float, Timeout, None, NotGiven]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CreateOptions(TypedDict, total=False):
|
|
29
|
+
model: str
|
|
30
|
+
best_of: Union[int, NotGiven]
|
|
31
|
+
max_tokens: Union[int, NotGiven]
|
|
32
|
+
random_seed: Union[int, NotGiven]
|
|
33
|
+
stop: Union[List[str], str, NotGiven]
|
|
34
|
+
temperature: Union[float, NotGiven]
|
|
35
|
+
top_p: Union[float, NotGiven]
|
|
36
|
+
extra_headers: Optional[Headers]
|
|
37
|
+
extra_query: Optional[Query]
|
|
38
|
+
extra_body: Optional[Body]
|
|
39
|
+
timeout: Union[float, Timeout, None, NotGiven]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
logger = logging.Logger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _process_completion_data_chunk(choice: StreamingData) -> str:
|
|
46
|
+
text = choice.value
|
|
47
|
+
if isinstance(text, str):
|
|
48
|
+
return text
|
|
49
|
+
raise ValueError("Failed to retrieve text from completion stream")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _process_chat_data_chunk(chat_data: Chat) -> dict:
|
|
53
|
+
choices = chat_data.choices
|
|
54
|
+
for entry in choices:
|
|
55
|
+
dict_entry = cast(dict, entry)
|
|
56
|
+
message = cast(dict, dict_entry["message"])
|
|
57
|
+
return message
|
|
58
|
+
raise ValueError("Failed to retrieve text from chat stream")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class WriterAIManager:
|
|
62
|
+
"""
|
|
63
|
+
Manages authentication for Writer AI functionalities.
|
|
64
|
+
|
|
65
|
+
:ivar token: Authentication token for the Writer AI API.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, token: Optional[str] = None):
|
|
69
|
+
"""
|
|
70
|
+
Initializes a WriterAIManager instance.
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
:param token: Optional; the default token for API authentication used if WRITER_API_KEY environment variable is not set up.
|
|
74
|
+
:raises RuntimeError: If an API key was not provided to initialize SDK client properly.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
self.client = Writer(
|
|
78
|
+
# This is the default and can be omitted
|
|
79
|
+
api_key=token,
|
|
80
|
+
)
|
|
81
|
+
except WriterError:
|
|
82
|
+
raise RuntimeError(
|
|
83
|
+
"Failed to acquire Writer API key. " +
|
|
84
|
+
"Provide it by either setting a WRITER_API_KEY" +
|
|
85
|
+
" environment variable, or by initializing the" +
|
|
86
|
+
" AI module explicitly: writer.ai.init(\"my-writer-api-key\")"
|
|
87
|
+
) from None
|
|
88
|
+
current_process = get_app_process()
|
|
89
|
+
setattr(current_process, 'ai_manager', self)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def acquire_instance(cls) -> 'WriterAIManager':
|
|
93
|
+
"""
|
|
94
|
+
Retrieve the existing instance of WriterAIManager from the current app process.
|
|
95
|
+
If no instance was previously initialized, creates a new one and attaches it to the current app process.
|
|
96
|
+
|
|
97
|
+
:returns: The current instance of the manager.
|
|
98
|
+
"""
|
|
99
|
+
instance: WriterAIManager
|
|
100
|
+
current_process = get_app_process()
|
|
101
|
+
|
|
102
|
+
# If instance was not created explicitly, we initialize a new one
|
|
103
|
+
try:
|
|
104
|
+
instance = getattr(current_process, 'ai_manager')
|
|
105
|
+
except AttributeError:
|
|
106
|
+
instance = cls()
|
|
107
|
+
return instance
|
|
108
|
+
|
|
109
|
+
@classmethod
|
|
110
|
+
def authorize(cls, token: str):
|
|
111
|
+
"""
|
|
112
|
+
Authorize the WriterAIManager with a new token.
|
|
113
|
+
This can be done as an alternative to setting up an environment variable, or to override the token that was already provided before.
|
|
114
|
+
|
|
115
|
+
:param token: The new token to use for authentication.
|
|
116
|
+
"""
|
|
117
|
+
instance = cls.acquire_instance()
|
|
118
|
+
instance.client = Writer(api_key=token)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def use_chat_model(cls) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Get the configuration for the chat model.
|
|
124
|
+
|
|
125
|
+
:returns: Name for the chat model.
|
|
126
|
+
"""
|
|
127
|
+
return "palmyra-x-002-32k"
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def use_completion_model(cls) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Get the configuration for the completion model.
|
|
133
|
+
|
|
134
|
+
:returns: Name for the completion model.
|
|
135
|
+
"""
|
|
136
|
+
return "palmyra-x-002-instruct"
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def acquire_client(cls) -> Writer:
|
|
140
|
+
instance = cls.acquire_instance()
|
|
141
|
+
return instance.client
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Conversation:
|
|
145
|
+
"""
|
|
146
|
+
Manages messages within a conversation flow with an AI system, including message validation,
|
|
147
|
+
history management, and communication with an AI model.
|
|
148
|
+
|
|
149
|
+
The Conversation class can be initialized in two ways:
|
|
150
|
+
1. By providing an initial system prompt as a string. This starts a new conversation, adding a system message with the provided prompt.
|
|
151
|
+
Example:
|
|
152
|
+
>>> conversation = Conversation("You are a social media expert in the financial industry")
|
|
153
|
+
2. By providing a history of messages as a list. This initializes the conversation with existing message data.
|
|
154
|
+
Example:
|
|
155
|
+
>>> history = [{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi, how can I help?"}]
|
|
156
|
+
>>> conversation = Conversation(history)
|
|
157
|
+
|
|
158
|
+
The class supports both class-wide configuration, which affects the entire conversation, and call-specific configuration,
|
|
159
|
+
which can override or complement the class-wide settings for specific method calls.
|
|
160
|
+
|
|
161
|
+
:param prompt_or_history: Initial system prompt as a string, or history of messages as a list, used to start a new conversation or to load an existing one.
|
|
162
|
+
:param config: Configuration settings for the conversation. These settings can include parameters such as `max_tokens`, `temperature`, and `timeout`,
|
|
163
|
+
which affect the behavior and performance of the conversation operations. This configuration provides a default context for all operations,
|
|
164
|
+
but can be overridden or extended by additional configurations passed directly to specific methods.
|
|
165
|
+
|
|
166
|
+
Configuration Example:
|
|
167
|
+
When initializing, you might provide a general configuration:
|
|
168
|
+
>>> config = {'max_tokens': 100, 'temperature': 0.5}
|
|
169
|
+
>>> conversation = Conversation("Initial prompt", config=config)
|
|
170
|
+
|
|
171
|
+
Later, when calling `complete` or `stream_complete`, you can override or extend the initial configuration:
|
|
172
|
+
>>> response = conversation.complete(data={'max_tokens': 150, 'temperature': 0.7})
|
|
173
|
+
This would increase the `max_tokens` limit to 150 and adjust the `temperature` to 0.7 for this specific call.
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
class Message(TypedDict):
|
|
177
|
+
"""
|
|
178
|
+
Typed dictionary for conversation messages.
|
|
179
|
+
|
|
180
|
+
:param role: Specifies the sender role.
|
|
181
|
+
:param content: Text content of the message.
|
|
182
|
+
:param actions: Optional dictionary containing actions related to the message.
|
|
183
|
+
"""
|
|
184
|
+
role: Literal["system", "assistant", "user"]
|
|
185
|
+
content: str
|
|
186
|
+
actions: Optional[dict]
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def validate_message(cls, message):
|
|
190
|
+
"""
|
|
191
|
+
Validates if the provided message dictionary matches the required structure and values.
|
|
192
|
+
|
|
193
|
+
:param message: The message to validate.
|
|
194
|
+
:raises ValueError: If the message structure is incorrect or values are inappropriate.
|
|
195
|
+
"""
|
|
196
|
+
if not isinstance(message, dict):
|
|
197
|
+
raise ValueError(f"Attempted to add a non-dict object to the Conversation: {message}")
|
|
198
|
+
if not ("role" in message and "content" in message):
|
|
199
|
+
raise ValueError(f"Improper message format to add to Conversation: {message}")
|
|
200
|
+
if not (isinstance(message["content"], str)):
|
|
201
|
+
raise ValueError(f"Non-string content in message cannot be added: {message}")
|
|
202
|
+
if message["role"] not in ["system", "assistant", "user"]:
|
|
203
|
+
raise ValueError(f"Unsupported role in message: {message}")
|
|
204
|
+
|
|
205
|
+
def __init__(self, prompt_or_history: Optional[Union[str, List['Conversation.Message']]] = None, config: Optional[ChatOptions] = None):
|
|
206
|
+
"""
|
|
207
|
+
Initializes a new conversation. Two options are possible:
|
|
208
|
+
|
|
209
|
+
1. With a system prompt.
|
|
210
|
+
|
|
211
|
+
:param system_prompt: The initial message from the system to start the conversation.
|
|
212
|
+
:param config: Optional configuration settings for the conversation.
|
|
213
|
+
|
|
214
|
+
Example:
|
|
215
|
+
>>> conversation = Conversation("You are a social media expert in the financial industry")
|
|
216
|
+
|
|
217
|
+
2. With a history of past messages.
|
|
218
|
+
|
|
219
|
+
:param history_import: A list of messages that form the history of the conversation.
|
|
220
|
+
:param config: Optional configuration settings for the conversation.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> history = [{"role": "user", "content": "Hello"}, {"role": "assistant", "content": "Hi, how can I help?"}]
|
|
224
|
+
>>> conversation = Conversation(history)
|
|
225
|
+
"""
|
|
226
|
+
self.messages: List['Conversation.Message'] = []
|
|
227
|
+
if isinstance(prompt_or_history, str):
|
|
228
|
+
# Working with a prompt: adding a system message to history
|
|
229
|
+
prompt = prompt_or_history
|
|
230
|
+
self.add("system", prompt)
|
|
231
|
+
elif isinstance(prompt_or_history, list):
|
|
232
|
+
# Working with a history: trying to add messages
|
|
233
|
+
history_import = prompt_or_history
|
|
234
|
+
for message in history_import:
|
|
235
|
+
self.validate_message(message)
|
|
236
|
+
self += message
|
|
237
|
+
|
|
238
|
+
self.config = config or {}
|
|
239
|
+
|
|
240
|
+
def _merge_chunk_to_last_message(self, raw_chunk: dict):
|
|
241
|
+
"""
|
|
242
|
+
Merge a chunk of data into the last message in the conversation.
|
|
243
|
+
|
|
244
|
+
This method takes a chunk of data and integrates it into the content of the
|
|
245
|
+
last message of the conversation. It appends additional content if present,
|
|
246
|
+
and merges other key-value pairs into the last message's dictionary.
|
|
247
|
+
|
|
248
|
+
:param raw_chunk: A dictionary containing the chunk of data to be merged.
|
|
249
|
+
:raises ValueError: If the Conversation's `messages` list is empty, indicating there is no message to merge the chunk with.
|
|
250
|
+
"""
|
|
251
|
+
def _clear_chunk_flag(chunk):
|
|
252
|
+
return {key: value for key, value in chunk.items() if key != "chunk"}
|
|
253
|
+
if not self.messages:
|
|
254
|
+
raise ValueError("No message to merge chunk with")
|
|
255
|
+
clear_chunk = _clear_chunk_flag(raw_chunk)
|
|
256
|
+
updated_last_message: 'Conversation.Message' = self.messages[-1]
|
|
257
|
+
if "content" in clear_chunk:
|
|
258
|
+
updated_last_message["content"] += clear_chunk.pop("content")
|
|
259
|
+
updated_last_message |= clear_chunk
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def _prepare_message(message: 'Conversation.Message') -> WriterAIMessage:
|
|
263
|
+
"""
|
|
264
|
+
Converts a message object stored in Conversation to a Writer AI SDK `Message` model, suitable for calls to API.
|
|
265
|
+
|
|
266
|
+
:param raw_chunk: The data to be merged into the last message.
|
|
267
|
+
:raises ValueError: If there are no messages in the conversation to merge with.
|
|
268
|
+
"""
|
|
269
|
+
if not ("role" in message and "content" in message):
|
|
270
|
+
raise ValueError("Improper message format")
|
|
271
|
+
return WriterAIMessage(content=message["content"], role=message["role"])
|
|
272
|
+
|
|
273
|
+
def __add__(self, chunk_or_message: Union['Conversation.Message', dict]):
|
|
274
|
+
"""
|
|
275
|
+
Adds a message or appends a chunk to the last message in the conversation.
|
|
276
|
+
|
|
277
|
+
:param chunk_or_message: Dictionary representation of a message or chunk to add.
|
|
278
|
+
:raises TypeError: If passed chunk_or_message is not a dictionary.
|
|
279
|
+
:raises ValueError: If chunk_or_message is not a proper message with "role" and "content".
|
|
280
|
+
"""
|
|
281
|
+
if not isinstance(chunk_or_message, dict):
|
|
282
|
+
raise TypeError("Conversation only supports dict operands for addition")
|
|
283
|
+
if chunk_or_message.get("chunk") is True:
|
|
284
|
+
chunk = chunk_or_message
|
|
285
|
+
self._merge_chunk_to_last_message(cast(dict, chunk))
|
|
286
|
+
else:
|
|
287
|
+
message = chunk_or_message
|
|
288
|
+
self.validate_message(message)
|
|
289
|
+
self.messages.append({"role": message["role"], "content": message["content"], "actions": message.get("actions")})
|
|
290
|
+
return self
|
|
291
|
+
|
|
292
|
+
def add(self, role: str, message: str):
|
|
293
|
+
"""
|
|
294
|
+
Adds a new message to the conversation.
|
|
295
|
+
|
|
296
|
+
:param role: The role of the message sender.
|
|
297
|
+
:param message: The content of the message.
|
|
298
|
+
"""
|
|
299
|
+
self.__add__({"role": role, "content": message})
|
|
300
|
+
|
|
301
|
+
def complete(self, config: Optional['ChatOptions'] = None) -> 'Conversation.Message':
|
|
302
|
+
"""
|
|
303
|
+
Processes the conversation with the current messages and additional data to generate a response.
|
|
304
|
+
Note: this method only produces AI model output and does not attach the result to the existing conversation history.
|
|
305
|
+
|
|
306
|
+
:param config: Optional parameters to pass for processing.
|
|
307
|
+
:return: Generated message.
|
|
308
|
+
:raises RuntimeError: If response data was not properly formatted to retrieve model text.
|
|
309
|
+
"""
|
|
310
|
+
if not config:
|
|
311
|
+
config = {'max_tokens': 2048}
|
|
312
|
+
|
|
313
|
+
client = WriterAIManager.acquire_client()
|
|
314
|
+
passed_messages: Iterable[WriterAIMessage] = [self._prepare_message(message) for message in self.messages]
|
|
315
|
+
request_data: ChatOptions = {**config, **self.config}
|
|
316
|
+
request_model = request_data.get("model") or WriterAIManager.use_chat_model()
|
|
317
|
+
|
|
318
|
+
response_data: Chat = client.chat.chat(
|
|
319
|
+
messages=passed_messages,
|
|
320
|
+
model=request_model,
|
|
321
|
+
max_tokens=request_data.get('max_tokens', NotGiven()),
|
|
322
|
+
n=request_data.get('n', NotGiven()),
|
|
323
|
+
stop=request_data.get('stop', NotGiven()),
|
|
324
|
+
temperature=request_data.get('temperature', NotGiven()),
|
|
325
|
+
top_p=request_data.get('top_p', NotGiven()),
|
|
326
|
+
extra_headers=request_data.get('extra_headers'),
|
|
327
|
+
extra_query=request_data.get('extra_query'),
|
|
328
|
+
extra_body=request_data.get('extra_body'),
|
|
329
|
+
timeout=request_data.get('timeout', NotGiven())
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
for entry in response_data.choices:
|
|
333
|
+
message = entry.message
|
|
334
|
+
if message:
|
|
335
|
+
return cast(Conversation.Message, message.model_dump())
|
|
336
|
+
raise RuntimeError(f"Failed to acquire proper response for completion from data: {response_data}")
|
|
337
|
+
|
|
338
|
+
def stream_complete(self, config: Optional['ChatOptions'] = None) -> Generator[dict, None, None]:
|
|
339
|
+
"""
|
|
340
|
+
Initiates a stream to receive chunks of the model's reply.
|
|
341
|
+
Note: this method only produces AI model output and does not attach the result to the existing conversation history.
|
|
342
|
+
|
|
343
|
+
:param config: Optional parameters to pass for processing.
|
|
344
|
+
:yields: Model response chunks as they arrive from the stream.
|
|
345
|
+
"""
|
|
346
|
+
if not config:
|
|
347
|
+
config = {'max_tokens': 2048}
|
|
348
|
+
|
|
349
|
+
client = WriterAIManager.acquire_client()
|
|
350
|
+
passed_messages: Iterable[WriterAIMessage] = [self._prepare_message(message) for message in self.messages]
|
|
351
|
+
request_data: ChatOptions = {**config, **self.config}
|
|
352
|
+
request_model = request_data.get("model") or WriterAIManager.use_chat_model()
|
|
353
|
+
|
|
354
|
+
response: Stream = client.chat.chat(
|
|
355
|
+
messages=passed_messages,
|
|
356
|
+
model=request_model,
|
|
357
|
+
stream=True,
|
|
358
|
+
max_tokens=request_data.get('max_tokens', NotGiven()),
|
|
359
|
+
n=request_data.get('n', NotGiven()),
|
|
360
|
+
stop=request_data.get('stop', NotGiven()),
|
|
361
|
+
temperature=request_data.get('temperature', NotGiven()),
|
|
362
|
+
top_p=request_data.get('top_p', NotGiven()),
|
|
363
|
+
extra_headers=request_data.get('extra_headers'),
|
|
364
|
+
extra_query=request_data.get('extra_query'),
|
|
365
|
+
extra_body=request_data.get('extra_body'),
|
|
366
|
+
timeout=request_data.get('timeout'),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# We avoid flagging first chunk
|
|
370
|
+
# to trigger creating a message
|
|
371
|
+
# to append chunks to
|
|
372
|
+
flag_chunks = False
|
|
373
|
+
|
|
374
|
+
for line in response:
|
|
375
|
+
chunk = _process_chat_data_chunk(line)
|
|
376
|
+
if flag_chunks is True:
|
|
377
|
+
chunk |= {"chunk": True}
|
|
378
|
+
if flag_chunks is False:
|
|
379
|
+
flag_chunks = True
|
|
380
|
+
yield chunk
|
|
381
|
+
else:
|
|
382
|
+
response.close()
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def serialized_messages(self) -> List['Message']:
|
|
386
|
+
"""
|
|
387
|
+
Returns a representation of the conversation, excluding system messages.
|
|
388
|
+
|
|
389
|
+
:return: List of messages without system messages.
|
|
390
|
+
"""
|
|
391
|
+
# Excluding system messages for privacy & security reasons
|
|
392
|
+
serialized_messages = \
|
|
393
|
+
[message for message in self.messages if message["role"] != "system"]
|
|
394
|
+
return serialized_messages
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def complete(initial_text: str, config: Optional['CreateOptions'] = None) -> str:
|
|
398
|
+
"""
|
|
399
|
+
Completes the input text using the given data and returns the first resulting text choice.
|
|
400
|
+
|
|
401
|
+
:param initial_text: The initial text prompt for the completion.
|
|
402
|
+
:param config: Optional dictionary containing parameters for the completion call.
|
|
403
|
+
:return: The text of the first choice from the completion response.
|
|
404
|
+
:raises RuntimeError: If response data was not properly formatted to retrieve model text.
|
|
405
|
+
"""
|
|
406
|
+
if not config:
|
|
407
|
+
config = {}
|
|
408
|
+
|
|
409
|
+
client = WriterAIManager.acquire_client()
|
|
410
|
+
request_model = config.get("model", None) or WriterAIManager.use_completion_model()
|
|
411
|
+
|
|
412
|
+
response_data: Completion = client.completions.create(
|
|
413
|
+
model=request_model,
|
|
414
|
+
prompt=initial_text,
|
|
415
|
+
best_of=config.get("best_of", NotGiven()),
|
|
416
|
+
max_tokens=config.get("max_tokens", NotGiven()),
|
|
417
|
+
random_seed=config.get("random_seed", NotGiven()),
|
|
418
|
+
stop=config.get("stop", NotGiven()),
|
|
419
|
+
temperature=config.get("temperature", NotGiven()),
|
|
420
|
+
top_p=config.get("top_p", NotGiven()),
|
|
421
|
+
extra_headers=config.get("extra_headers"),
|
|
422
|
+
extra_body=config.get("extra_body"),
|
|
423
|
+
extra_query=config.get("extra_query"),
|
|
424
|
+
timeout=config.get("timeout")
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
for entry in response_data.choices:
|
|
428
|
+
text = entry.text
|
|
429
|
+
if text:
|
|
430
|
+
return text
|
|
431
|
+
|
|
432
|
+
raise RuntimeError(f"Failed to acquire proper response for completion from data: {response_data}")
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def stream_complete(initial_text: str, config: Optional['CreateOptions'] = None) -> Generator[str, None, None]:
|
|
436
|
+
"""
|
|
437
|
+
Streams completion results from an initial text prompt, yielding each piece of text as it is received.
|
|
438
|
+
|
|
439
|
+
:param initial_text: The initial text prompt for the stream completion.
|
|
440
|
+
:param config: Optional dictionary containing parameters for the stream completion call.
|
|
441
|
+
:yields: Each text completion as it arrives from the stream.
|
|
442
|
+
"""
|
|
443
|
+
if not config:
|
|
444
|
+
config = {"max_tokens": 2048}
|
|
445
|
+
|
|
446
|
+
client = WriterAIManager.acquire_client()
|
|
447
|
+
request_model = config.get("model", None) or WriterAIManager.use_completion_model()
|
|
448
|
+
|
|
449
|
+
response: Stream = client.completions.create(
|
|
450
|
+
model=request_model,
|
|
451
|
+
prompt=initial_text,
|
|
452
|
+
stream=True,
|
|
453
|
+
best_of=config.get("best_of", NotGiven()),
|
|
454
|
+
max_tokens=config.get("max_tokens", NotGiven()),
|
|
455
|
+
random_seed=config.get("random_seed", NotGiven()),
|
|
456
|
+
stop=config.get("stop", NotGiven()),
|
|
457
|
+
temperature=config.get("temperature", NotGiven()),
|
|
458
|
+
top_p=config.get("top_p", NotGiven()),
|
|
459
|
+
extra_headers=config.get("extra_headers"),
|
|
460
|
+
extra_body=config.get("extra_body"),
|
|
461
|
+
extra_query=config.get("extra_query"),
|
|
462
|
+
timeout=config.get("timeout")
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
for line in response:
|
|
466
|
+
processed_line = _process_completion_data_chunk(line)
|
|
467
|
+
if processed_line:
|
|
468
|
+
yield processed_line
|
|
469
|
+
else:
|
|
470
|
+
response.close()
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def init(token: Optional[str] = None):
|
|
474
|
+
"""
|
|
475
|
+
Initializes the WriterAIManager with an optional token.
|
|
476
|
+
|
|
477
|
+
:param token: Optional token for authentication.
|
|
478
|
+
:return: An instance of WriterAIManager.
|
|
479
|
+
"""
|
|
480
|
+
return WriterAIManager(token=token)
|