langroid 0.10.2__py3-none-any.whl → 0.12.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.
@@ -3,12 +3,14 @@ Various tools to for agents to be able to control flow of Task, e.g.
3
3
  termination, routing to another agent, etc.
4
4
  """
5
5
 
6
- from typing import List, Tuple
6
+ from typing import Any, List, Tuple
7
7
 
8
8
  from langroid.agent.chat_agent import ChatAgent
9
9
  from langroid.agent.chat_document import ChatDocument
10
10
  from langroid.agent.tool_message import ToolMessage
11
11
  from langroid.mytypes import Entity
12
+ from langroid.pydantic_v1 import Extra
13
+ from langroid.utils.types import to_string
12
14
 
13
15
 
14
16
  class AgentDoneTool(ToolMessage):
@@ -17,16 +19,20 @@ class AgentDoneTool(ToolMessage):
17
19
 
18
20
  purpose: str = """
19
21
  To signal the current task is done, along with an optional message <content>
20
- (default empty string) and an optional list of <tools> (default empty list).
22
+ of arbitrary type (default None) and an
23
+ optional list of <tools> (default empty list).
21
24
  """
22
25
  request: str = "agent_done_tool"
23
- content: str = ""
26
+ content: Any = None
24
27
  tools: List[ToolMessage] = []
25
- _handle_only: bool = True
28
+ # only meant for agent_response or tool-handlers, not for LLM generation:
29
+ _allow_llm_use: bool = False
26
30
 
27
31
  def response(self, agent: ChatAgent) -> ChatDocument:
32
+ content_str = "" if self.content is None else to_string(self.content)
28
33
  return agent.create_agent_response(
29
- self.content,
34
+ content=content_str,
35
+ content_any=self.content,
30
36
  tool_messages=[self] + self.tools,
31
37
  )
32
38
 
@@ -37,14 +43,15 @@ class DoneTool(ToolMessage):
37
43
 
38
44
  purpose = """
39
45
  To signal the current task is done, along with an optional message <content>
40
- (default empty string).
46
+ of arbitrary type (default None).
41
47
  """
42
48
  request = "done_tool"
43
49
  content: str = ""
44
50
 
45
51
  def response(self, agent: ChatAgent) -> ChatDocument:
46
52
  return agent.create_agent_response(
47
- self.content,
53
+ content=self.content,
54
+ content_any=self.content,
48
55
  tool_messages=[self],
49
56
  )
50
57
 
@@ -58,6 +65,78 @@ class DoneTool(ToolMessage):
58
65
  """
59
66
 
60
67
 
68
+ class ResultTool(ToolMessage):
69
+ """Class to use as a wrapper for sending arbitrary results from an Agent's
70
+ agent_response or tool handlers, to:
71
+ (a) trigger completion of the current task (similar to (Agent)DoneTool), and
72
+ (b) be returned as the result of the current task, i.e. this tool would appear
73
+ in the resulting ChatDocument's `tool_messages` list.
74
+ See test_tool_handlers_and_results in test_tool_messages.py, and
75
+ examples/basic/tool-extract-short-example.py.
76
+
77
+ Note:
78
+ - when defining a tool handler or agent_response, you can directly return
79
+ ResultTool(field1 = val1, ...),
80
+ where the values can be aribitrary data structures, including nested
81
+ Pydantic objs, or you can define a subclass of ResultTool with the
82
+ fields you want to return.
83
+ - This is a special ToolMessage that is NOT meant to be used or handled
84
+ by an agent.
85
+ - AgentDoneTool is more restrictive in that you can only send a `content`
86
+ or `tools` in the result.
87
+ """
88
+
89
+ request: str = "result_tool"
90
+ purpose: str = "Ignored; Wrapper for a structured message"
91
+ id: str = "" # placeholder for OpenAI-API tool_call_id
92
+
93
+ class Config:
94
+ extra = Extra.allow
95
+ arbitrary_types_allowed = False
96
+ validate_all = True
97
+ validate_assignment = True
98
+ # do not include these fields in the generated schema
99
+ # since we don't require the LLM to specify them
100
+ schema_extra = {"exclude": {"purpose", "id"}}
101
+
102
+ def handle(self) -> AgentDoneTool:
103
+ return AgentDoneTool(tools=[self])
104
+
105
+
106
+ class FinalResultTool(ToolMessage):
107
+ """Class to use as a wrapper for sending arbitrary results from an Agent's
108
+ agent_response or tool handlers, to:
109
+ (a) trigger completion of the current task as well as all parent tasks, and
110
+ (b) be returned as the final result of the root task, i.e. this tool would appear
111
+ in the final ChatDocument's `tool_messages` list.
112
+ See test_tool_handlers_and_results in test_tool_messages.py, and
113
+ examples/basic/tool-extract-short-example.py.
114
+
115
+ Note:
116
+ - when defining a tool handler or agent_response, you can directly return
117
+ FinalResultTool(field1 = val1, ...),
118
+ where the values can be aribitrary data structures, including nested
119
+ Pydantic objs, or you can define a subclass of FinalResultTool with the
120
+ fields you want to return.
121
+ - This is a special ToolMessage that is NOT meant to be used or handled
122
+ by an agent.
123
+ """
124
+
125
+ request: str = ""
126
+ purpose: str = "Ignored; Wrapper for a structured message"
127
+ id: str = "" # placeholder for OpenAI-API tool_call_id
128
+ _allow_llm_use: bool = False
129
+
130
+ class Config:
131
+ extra = Extra.allow
132
+ arbitrary_types_allowed = False
133
+ validate_all = True
134
+ validate_assignment = True
135
+ # do not include these fields in the generated schema
136
+ # since we don't require the LLM to specify them
137
+ schema_extra = {"exclude": {"purpose", "id"}}
138
+
139
+
61
140
  class PassTool(ToolMessage):
62
141
  """Tool for "passing" on the received msg (ChatDocument),
63
142
  so that an as-yet-unspecified agent can handle it.
@@ -206,7 +285,7 @@ class AgentSendTool(ToolMessage):
206
285
  to: str
207
286
  content: str = ""
208
287
  tools: List[ToolMessage] = []
209
- _handle_only: bool = True
288
+ _allow_llm_use: bool = False
210
289
 
211
290
  def response(self, agent: ChatAgent) -> ChatDocument:
212
291
  return agent.create_agent_response(
@@ -1,6 +1,6 @@
1
1
  """Mock Language Model for testing"""
2
2
 
3
- from typing import Callable, Dict, List, Optional, Union
3
+ from typing import Awaitable, Callable, Dict, List, Optional, Union
4
4
 
5
5
  import langroid.language_models as lm
6
6
  from langroid.language_models import LLMResponse
@@ -10,6 +10,7 @@ from langroid.language_models.base import (
10
10
  OpenAIToolSpec,
11
11
  ToolChoiceTypes,
12
12
  )
13
+ from langroid.utils.types import to_string
13
14
 
14
15
 
15
16
  def none_fn(x: str) -> None | str:
@@ -27,6 +28,7 @@ class MockLMConfig(LLMConfig):
27
28
 
28
29
  response_dict: Dict[str, str] = {}
29
30
  response_fn: Callable[[str], None | str] = none_fn
31
+ response_fn_async: Optional[Callable[[str], Awaitable[Optional[str]]]] = None
30
32
  default_response: str = "Mock response"
31
33
 
32
34
  type: str = "mock"
@@ -43,11 +45,30 @@ class MockLM(LanguageModel):
43
45
  # - response_dict
44
46
  # - response_fn
45
47
  # - default_response
48
+ mapped_response = self.config.response_dict.get(
49
+ msg, self.config.response_fn(msg) or self.config.default_response
50
+ )
46
51
  return lm.LLMResponse(
47
- message=self.config.response_dict.get(
48
- msg,
49
- self.config.response_fn(msg) or self.config.default_response,
50
- ),
52
+ message=to_string(mapped_response),
53
+ cached=False,
54
+ )
55
+
56
+ async def _response_async(self, msg: str) -> LLMResponse:
57
+ # response is based on this fallback order:
58
+ # - response_dict
59
+ # - response_fn_async
60
+ # - response_fn
61
+ # - default_response
62
+ if self.config.response_fn_async is not None:
63
+ response = await self.config.response_fn_async(msg)
64
+ else:
65
+ response = self.config.response_fn(msg)
66
+
67
+ mapped_response = self.config.response_dict.get(
68
+ msg, response or self.config.default_response
69
+ )
70
+ return lm.LLMResponse(
71
+ message=to_string(mapped_response),
51
72
  cached=False,
52
73
  )
53
74
 
@@ -79,7 +100,7 @@ class MockLM(LanguageModel):
79
100
  Mock chat function for testing
80
101
  """
81
102
  last_msg = messages[-1].content if isinstance(messages, list) else messages
82
- return self._response(last_msg)
103
+ return await self._response_async(last_msg)
83
104
 
84
105
  def generate(self, prompt: str, max_tokens: int = 200) -> lm.LLMResponse:
85
106
  """
@@ -91,7 +112,7 @@ class MockLM(LanguageModel):
91
112
  """
92
113
  Mock generate function for testing
93
114
  """
94
- return self._response(prompt)
115
+ return await self._response_async(prompt)
95
116
 
96
117
  def get_stream(self) -> bool:
97
118
  return False
@@ -48,10 +48,13 @@ class WebSearchResult:
48
48
  return self.full_content[: self.max_summary_length]
49
49
 
50
50
  def get_full_content(self) -> str:
51
- response: Response = requests.get(self.link)
52
- soup: BeautifulSoup = BeautifulSoup(response.text, "lxml")
53
- text = " ".join(soup.stripped_strings)
54
- return text[: self.max_content_length]
51
+ try:
52
+ response: Response = requests.get(self.link)
53
+ soup: BeautifulSoup = BeautifulSoup(response.text, "lxml")
54
+ text = " ".join(soup.stripped_strings)
55
+ return text[: self.max_content_length]
56
+ except Exception as e:
57
+ return f"Error fetching content from {self.link}: {e}"
55
58
 
56
59
  def __str__(self) -> str:
57
60
  return f"Title: {self.title}\nLink: {self.link}\nSummary: {self.summary}"
@@ -0,0 +1,121 @@
1
+ [project]
2
+ # Whether to enable telemetry (default: true). No personal data is collected.
3
+ enable_telemetry = true
4
+
5
+
6
+ # List of environment variables to be provided by each user to use the app.
7
+ user_env = []
8
+
9
+ # Duration (in seconds) during which the session is saved when the connection is lost
10
+ session_timeout = 3600
11
+
12
+ # Enable third parties caching (e.g LangChain cache)
13
+ cache = false
14
+
15
+ # Authorized origins
16
+ allow_origins = ["*"]
17
+
18
+ # Follow symlink for asset mount (see https://github.com/Chainlit/chainlit/issues/317)
19
+ # follow_symlink = false
20
+
21
+ [features]
22
+ # Show the prompt playground
23
+ prompt_playground = true
24
+
25
+ # Process and display HTML in messages. This can be a security risk (see https://stackoverflow.com/questions/19603097/why-is-it-dangerous-to-render-user-generated-html-or-javascript)
26
+ unsafe_allow_html = false
27
+
28
+ # Process and display mathematical expressions. This can clash with "$" characters in messages.
29
+ latex = false
30
+
31
+ # Automatically tag threads with the current chat profile (if a chat profile is used)
32
+ auto_tag_thread = true
33
+
34
+ # Authorize users to spontaneously upload files with messages
35
+ [features.spontaneous_file_upload]
36
+ enabled = true
37
+ accept = ["*/*"]
38
+ max_files = 20
39
+ max_size_mb = 500
40
+
41
+ [features.audio]
42
+ # Threshold for audio recording
43
+ min_decibels = -45
44
+ # Delay for the user to start speaking in MS
45
+ initial_silence_timeout = 3000
46
+ # Delay for the user to continue speaking in MS. If the user stops speaking for this duration, the recording will stop.
47
+ silence_timeout = 1500
48
+ # Above this duration (MS), the recording will forcefully stop.
49
+ max_duration = 15000
50
+ # Duration of the audio chunks in MS
51
+ chunk_duration = 1000
52
+ # Sample rate of the audio
53
+ sample_rate = 44100
54
+
55
+ [UI]
56
+ # Name of the app and chatbot.
57
+ name = "Chatbot"
58
+
59
+ # Show the readme while the thread is empty.
60
+ show_readme_as_default = true
61
+
62
+ # Description of the app and chatbot. This is used for HTML tags.
63
+ # description = ""
64
+
65
+ # Large size content are by default collapsed for a cleaner ui
66
+ default_collapse_content = true
67
+
68
+ # The default value for the expand messages settings.
69
+ default_expand_messages = false
70
+
71
+ # Hide the chain of thought details from the user in the UI.
72
+ hide_cot = false
73
+
74
+ # Link to your github repo. This will add a github button in the UI's header.
75
+ # github = ""
76
+
77
+ # Specify a CSS file that can be used to customize the user interface.
78
+ # The CSS file can be served from the public directory or via an external link.
79
+ # custom_css = "/public/test.css"
80
+
81
+ # Specify a Javascript file that can be used to customize the user interface.
82
+ # The Javascript file can be served from the public directory.
83
+ # custom_js = "/public/test.js"
84
+
85
+ # Specify a custom font url.
86
+ # custom_font = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"
87
+
88
+ # Specify a custom meta image url.
89
+ # custom_meta_image_url = "https://chainlit-cloud.s3.eu-west-3.amazonaws.com/logo/chainlit_banner.png"
90
+
91
+ # Specify a custom build directory for the frontend.
92
+ # This can be used to customize the frontend code.
93
+ # Be careful: If this is a relative path, it should not start with a slash.
94
+ # custom_build = "./public/build"
95
+
96
+ [UI.theme]
97
+ #layout = "wide"
98
+ #font_family = "Inter, sans-serif"
99
+ # Override default MUI light theme. (Check theme.ts)
100
+ [UI.theme.light]
101
+ #background = "#FAFAFA"
102
+ #paper = "#FFFFFF"
103
+
104
+ [UI.theme.light.primary]
105
+ #main = "#F80061"
106
+ #dark = "#980039"
107
+ #light = "#FFE7EB"
108
+
109
+ # Override default MUI dark theme. (Check theme.ts)
110
+ [UI.theme.dark]
111
+ #background = "#FAFAFA"
112
+ #paper = "#FFFFFF"
113
+
114
+ [UI.theme.dark.primary]
115
+ #main = "#F80061"
116
+ #dark = "#980039"
117
+ #light = "#FFE7EB"
118
+
119
+
120
+ [meta]
121
+ generated_by = "1.1.202"
@@ -0,0 +1,231 @@
1
+ {
2
+ "components": {
3
+ "atoms": {
4
+ "buttons": {
5
+ "userButton": {
6
+ "menu": {
7
+ "settings": "Settings",
8
+ "settingsKey": "S",
9
+ "APIKeys": "API Keys",
10
+ "logout": "Logout"
11
+ }
12
+ }
13
+ }
14
+ },
15
+ "molecules": {
16
+ "newChatButton": {
17
+ "newChat": "New Chat"
18
+ },
19
+ "tasklist": {
20
+ "TaskList": {
21
+ "title": "\ud83d\uddd2\ufe0f Task List",
22
+ "loading": "Loading...",
23
+ "error": "An error occured"
24
+ }
25
+ },
26
+ "attachments": {
27
+ "cancelUpload": "Cancel upload",
28
+ "removeAttachment": "Remove attachment"
29
+ },
30
+ "newChatDialog": {
31
+ "createNewChat": "Create new chat?",
32
+ "clearChat": "This will clear the current messages and start a new chat.",
33
+ "cancel": "Cancel",
34
+ "confirm": "Confirm"
35
+ },
36
+ "settingsModal": {
37
+ "settings": "Settings",
38
+ "expandMessages": "Expand Messages",
39
+ "hideChainOfThought": "Hide Chain of Thought",
40
+ "darkMode": "Dark Mode"
41
+ },
42
+ "detailsButton": {
43
+ "using": "Using",
44
+ "running": "Running",
45
+ "took_one": "Took {{count}} step",
46
+ "took_other": "Took {{count}} steps"
47
+ },
48
+ "auth": {
49
+ "authLogin": {
50
+ "title": "Login to access the app.",
51
+ "form": {
52
+ "email": "Email address",
53
+ "password": "Password",
54
+ "noAccount": "Don't have an account?",
55
+ "alreadyHaveAccount": "Already have an account?",
56
+ "signup": "Sign Up",
57
+ "signin": "Sign In",
58
+ "or": "OR",
59
+ "continue": "Continue",
60
+ "forgotPassword": "Forgot password?",
61
+ "passwordMustContain": "Your password must contain:",
62
+ "emailRequired": "email is a required field",
63
+ "passwordRequired": "password is a required field"
64
+ },
65
+ "error": {
66
+ "default": "Unable to sign in.",
67
+ "signin": "Try signing in with a different account.",
68
+ "oauthsignin": "Try signing in with a different account.",
69
+ "redirect_uri_mismatch": "The redirect URI is not matching the oauth app configuration.",
70
+ "oauthcallbackerror": "Try signing in with a different account.",
71
+ "oauthcreateaccount": "Try signing in with a different account.",
72
+ "emailcreateaccount": "Try signing in with a different account.",
73
+ "callback": "Try signing in with a different account.",
74
+ "oauthaccountnotlinked": "To confirm your identity, sign in with the same account you used originally.",
75
+ "emailsignin": "The e-mail could not be sent.",
76
+ "emailverify": "Please verify your email, a new email has been sent.",
77
+ "credentialssignin": "Sign in failed. Check the details you provided are correct.",
78
+ "sessionrequired": "Please sign in to access this page."
79
+ }
80
+ },
81
+ "authVerifyEmail": {
82
+ "almostThere": "You're almost there! We've sent an email to ",
83
+ "verifyEmailLink": "Please click on the link in that email to complete your signup.",
84
+ "didNotReceive": "Can't find the email?",
85
+ "resendEmail": "Resend email",
86
+ "goBack": "Go Back",
87
+ "emailSent": "Email sent successfully.",
88
+ "verifyEmail": "Verify your email address"
89
+ },
90
+ "providerButton": {
91
+ "continue": "Continue with {{provider}}",
92
+ "signup": "Sign up with {{provider}}"
93
+ },
94
+ "authResetPassword": {
95
+ "newPasswordRequired": "New password is a required field",
96
+ "passwordsMustMatch": "Passwords must match",
97
+ "confirmPasswordRequired": "Confirm password is a required field",
98
+ "newPassword": "New password",
99
+ "confirmPassword": "Confirm password",
100
+ "resetPassword": "Reset Password"
101
+ },
102
+ "authForgotPassword": {
103
+ "email": "Email address",
104
+ "emailRequired": "email is a required field",
105
+ "emailSent": "Please check the email address {{email}} for instructions to reset your password.",
106
+ "enterEmail": "Enter your email address and we will send you instructions to reset your password.",
107
+ "resendEmail": "Resend email",
108
+ "continue": "Continue",
109
+ "goBack": "Go Back"
110
+ }
111
+ }
112
+ },
113
+ "organisms": {
114
+ "chat": {
115
+ "history": {
116
+ "index": {
117
+ "showHistory": "Show history",
118
+ "lastInputs": "Last Inputs",
119
+ "noInputs": "Such empty...",
120
+ "loading": "Loading..."
121
+ }
122
+ },
123
+ "inputBox": {
124
+ "input": {
125
+ "placeholder": "Type your message here..."
126
+ },
127
+ "speechButton": {
128
+ "start": "Start recording",
129
+ "stop": "Stop recording"
130
+ },
131
+ "SubmitButton": {
132
+ "sendMessage": "Send message",
133
+ "stopTask": "Stop Task"
134
+ },
135
+ "UploadButton": {
136
+ "attachFiles": "Attach files"
137
+ },
138
+ "waterMark": {
139
+ "text": "Built with"
140
+ }
141
+ },
142
+ "Messages": {
143
+ "index": {
144
+ "running": "Running",
145
+ "executedSuccessfully": "executed successfully",
146
+ "failed": "failed",
147
+ "feedbackUpdated": "Feedback updated",
148
+ "updating": "Updating"
149
+ }
150
+ },
151
+ "dropScreen": {
152
+ "dropYourFilesHere": "Drop your files here"
153
+ },
154
+ "index": {
155
+ "failedToUpload": "Failed to upload",
156
+ "cancelledUploadOf": "Cancelled upload of",
157
+ "couldNotReachServer": "Could not reach the server",
158
+ "continuingChat": "Continuing previous chat"
159
+ },
160
+ "settings": {
161
+ "settingsPanel": "Settings panel",
162
+ "reset": "Reset",
163
+ "cancel": "Cancel",
164
+ "confirm": "Confirm"
165
+ }
166
+ },
167
+ "threadHistory": {
168
+ "sidebar": {
169
+ "filters": {
170
+ "FeedbackSelect": {
171
+ "feedbackAll": "Feedback: All",
172
+ "feedbackPositive": "Feedback: Positive",
173
+ "feedbackNegative": "Feedback: Negative"
174
+ },
175
+ "SearchBar": {
176
+ "search": "Search"
177
+ }
178
+ },
179
+ "DeleteThreadButton": {
180
+ "confirmMessage": "This will delete the thread as well as it's messages and elements.",
181
+ "cancel": "Cancel",
182
+ "confirm": "Confirm",
183
+ "deletingChat": "Deleting chat",
184
+ "chatDeleted": "Chat deleted"
185
+ },
186
+ "index": {
187
+ "pastChats": "Past Chats"
188
+ },
189
+ "ThreadList": {
190
+ "empty": "Empty...",
191
+ "today": "Today",
192
+ "yesterday": "Yesterday",
193
+ "previous7days": "Previous 7 days",
194
+ "previous30days": "Previous 30 days"
195
+ },
196
+ "TriggerButton": {
197
+ "closeSidebar": "Close sidebar",
198
+ "openSidebar": "Open sidebar"
199
+ }
200
+ },
201
+ "Thread": {
202
+ "backToChat": "Go back to chat",
203
+ "chatCreatedOn": "This chat was created on"
204
+ }
205
+ },
206
+ "header": {
207
+ "chat": "Chat",
208
+ "readme": "Readme"
209
+ }
210
+ }
211
+ },
212
+ "hooks": {
213
+ "useLLMProviders": {
214
+ "failedToFetchProviders": "Failed to fetch providers:"
215
+ }
216
+ },
217
+ "pages": {
218
+ "Design": {},
219
+ "Env": {
220
+ "savedSuccessfully": "Saved successfully",
221
+ "requiredApiKeys": "Required API Keys",
222
+ "requiredApiKeysInfo": "To use this app, the following API keys are required. The keys are stored on your device's local storage."
223
+ },
224
+ "Page": {
225
+ "notPartOfProject": "You are not part of this project."
226
+ },
227
+ "ResumeButton": {
228
+ "resumeChat": "Resume Chat"
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,93 @@
1
+ import json
2
+ import logging
3
+ from typing import Any, Optional, Type, TypeVar, Union, get_args, get_origin
4
+
5
+ from langroid.pydantic_v1 import BaseModel
6
+
7
+ logger = logging.getLogger(__name__)
8
+ PrimitiveType = Union[int, float, bool, str]
9
+ T = TypeVar("T")
10
+
11
+
12
+ def is_instance_of(obj: Any, type_hint: Type[T] | Any) -> bool:
13
+ """
14
+ Check if an object is an instance of a type hint, e.g.
15
+ to check whether x is of type `List[ToolMessage]` or type `int`
16
+ """
17
+ if type_hint == Any:
18
+ return True
19
+
20
+ if type_hint is type(obj):
21
+ return True
22
+
23
+ origin = get_origin(type_hint)
24
+ args = get_args(type_hint)
25
+
26
+ if origin is Union:
27
+ return any(is_instance_of(obj, arg) for arg in args)
28
+
29
+ if origin: # e.g. List, Dict, Tuple, Set
30
+ if isinstance(obj, origin):
31
+ # check if all items in obj are of the required types
32
+ if args:
33
+ if isinstance(obj, (list, tuple, set)):
34
+ return all(is_instance_of(item, args[0]) for item in obj)
35
+ if isinstance(obj, dict):
36
+ return all(
37
+ is_instance_of(k, args[0]) and is_instance_of(v, args[1])
38
+ for k, v in obj.items()
39
+ )
40
+ return True
41
+ else:
42
+ return False
43
+
44
+ return isinstance(obj, type_hint)
45
+
46
+
47
+ def to_string(msg: Any) -> str:
48
+ """
49
+ Best-effort conversion of arbitrary msg to str.
50
+ Return empty string if conversion fails.
51
+ """
52
+ if msg is None:
53
+ return ""
54
+ if isinstance(msg, str):
55
+ return msg
56
+ if isinstance(msg, BaseModel):
57
+ return msg.json()
58
+ # last resort: use json.dumps() or str() to make it a str
59
+ try:
60
+ return json.dumps(msg)
61
+ except Exception:
62
+ try:
63
+ return str(msg)
64
+ except Exception as e:
65
+ logger.error(
66
+ f"""
67
+ Error converting msg to str: {e}",
68
+ """,
69
+ exc_info=True,
70
+ )
71
+ return ""
72
+
73
+
74
+ def from_string(
75
+ s: str,
76
+ output_type: Type[PrimitiveType],
77
+ ) -> Optional[PrimitiveType]:
78
+ if output_type is int:
79
+ try:
80
+ return int(s)
81
+ except ValueError:
82
+ return None
83
+ elif output_type is float:
84
+ try:
85
+ return float(s)
86
+ except ValueError:
87
+ return None
88
+ elif output_type is bool:
89
+ return s.lower() in ("true", "yes", "1")
90
+ elif output_type is str:
91
+ return s
92
+ else:
93
+ return None