Class representing an entry in the msg-history sent to the LLM API. It could be one of these: - a user message - an LLM ("Assistant") response - a fn-call or tool-call-list from an OpenAI-compatible LLM API response - a result or results from executing a fn or tool-call(s)
| 270 | |
| 271 | |
| 272 | class LLMMessage(BaseModel): |
| 273 | """ |
| 274 | Class representing an entry in the msg-history sent to the LLM API. |
| 275 | It could be one of these: |
| 276 | - a user message |
| 277 | - an LLM ("Assistant") response |
| 278 | - a fn-call or tool-call-list from an OpenAI-compatible LLM API response |
| 279 | - a result or results from executing a fn or tool-call(s) |
| 280 | """ |
| 281 | |
| 282 | role: Role |
| 283 | name: Optional[str] = None |
| 284 | tool_call_id: Optional[str] = None # which OpenAI LLM tool this is a response to |
| 285 | tool_id: str = "" # used by OpenAIAssistant |
| 286 | content: str |
| 287 | files: List[FileAttachment] = [] |
| 288 | function_call: Optional[LLMFunctionCall] = None |
| 289 | tool_calls: Optional[List[OpenAIToolCall]] = None |
| 290 | timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) |
| 291 | # link to corresponding chat document, for provenance/rewind purposes |
| 292 | chat_document_id: str = "" |
| 293 | |
| 294 | def api_dict(self, model: str, has_system_role: bool = True) -> Dict[str, Any]: |
| 295 | """ |
| 296 | Convert to dictionary for API request, keeping ONLY |
| 297 | the fields that are expected in an API call! |
| 298 | E.g., DROP the tool_id, since it is only for use in the Assistant API, |
| 299 | not the completion API. |
| 300 | |
| 301 | Args: |
| 302 | has_system_role: whether the message has a system role (if not, |
| 303 | set to "user" role) |
| 304 | Returns: |
| 305 | dict: dictionary representation of LLM message |
| 306 | """ |
| 307 | d = self.model_dump() |
| 308 | files: List[FileAttachment] = d.pop("files") |
| 309 | if len(files) > 0 and self.role == Role.USER: |
| 310 | # In there are files, then content is an array of |
| 311 | # different content-parts |
| 312 | d["content"] = [ |
| 313 | dict( |
| 314 | type="text", |
| 315 | text=self.content, |
| 316 | ) |
| 317 | ] + [f.to_dict(model) for f in self.files] |
| 318 | |
| 319 | # if there is a key k = "role" with value "system", change to "user" |
| 320 | # in case has_system_role is False |
| 321 | if not has_system_role and "role" in d and d["role"] == "system": |
| 322 | d["role"] = "user" |
| 323 | if "content" in d: |
| 324 | d["content"] = "[ADDITIONAL SYSTEM MESSAGE:]\n\n" + d["content"] |
| 325 | # drop None values since API doesn't accept them |
| 326 | dict_no_none = {k: v for k, v in d.items() if v is not None} |
| 327 | if "name" in dict_no_none and dict_no_none["name"] == "": |
| 328 | # OpenAI API does not like empty name |
| 329 | del dict_no_none["name"] |
no outgoing calls
searching dependent graphs…