dm-aioaiagent 0.3.2__py3-none-any.whl → 0.3.4__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.
dm_aioaiagent/__init__.py CHANGED
@@ -2,4 +2,5 @@ from dotenv import load_dotenv
2
2
  load_dotenv()
3
3
  from .ai_agent import DMAIAgent
4
4
  from .async_ai_agent import DMAioAIAgent
5
+ from .image_message_content_builder import ImageMessageContentBuilder
5
6
  from .types import Message
dm_aioaiagent/ai_agent.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from pydantic import SecretStr
2
3
  from itertools import dropwhile
3
4
  from threading import Thread
4
5
  from langchain_openai import ChatOpenAI
@@ -16,6 +17,8 @@ __all__ = ["DMAIAgent"]
16
17
  class DMAIAgent:
17
18
  agent_name = "AIAgent"
18
19
  _allowed_roles = ("user", "ai")
20
+ _response_if_request_fail = "I can't provide a response right now. Please try again later."
21
+ _response_if_invalid_image = "The image is unavailable or the link is incorrect."
19
22
  MAX_MEMORY_MESSAGES = 20 # Only INT greater than 0
20
23
 
21
24
  def __init__(
@@ -28,20 +31,29 @@ class DMAIAgent:
28
31
  agent_name: str = None,
29
32
  input_output_logging: bool = True,
30
33
  is_memory_enabled: bool = True,
34
+ save_tools_responses_in_memory: bool = True,
31
35
  max_memory_messages: int = None,
36
+ response_if_request_fail: str = None,
37
+ response_if_invalid_image: str = None,
38
+ openai_api_key: str = None
32
39
  ):
33
- if not os.getenv("OPENAI_API_KEY"):
40
+ if openai_api_key is None and not os.getenv("OPENAI_API_KEY"):
34
41
  raise EnvironmentError("'OPENAI_API_KEY' environment variable is not set!")
35
42
 
36
43
  self._logger = DMLogger(agent_name or self.agent_name)
37
44
  self._is_tools_exists = bool(tools)
38
45
  self._input_output_logging = bool(input_output_logging)
39
46
  self._is_memory_enabled = bool(is_memory_enabled)
47
+ self._save_tools_responses_in_memory = bool(save_tools_responses_in_memory)
40
48
  self._max_memory_messages = self._validate_max_memory_messages(max_memory_messages)
49
+ self._response_if_request_fail = str(response_if_request_fail or self._response_if_request_fail)
50
+ self._response_if_invalid_image = str(response_if_invalid_image or self._response_if_invalid_image)
41
51
 
42
52
  prompt = ChatPromptTemplate.from_messages([SystemMessage(content=system_message),
43
53
  MessagesPlaceholder(variable_name="messages")])
44
- llm = ChatOpenAI(model=str(model), temperature=int(temperature))
54
+ if openai_api_key:
55
+ openai_api_key = SecretStr(openai_api_key)
56
+ llm = ChatOpenAI(model_name=str(model), temperature=int(temperature), openai_api_key=openai_api_key)
45
57
  if self._is_tools_exists:
46
58
  self._tool_map = {t.name: t for t in tools}
47
59
  llm = llm.bind_tools(tools)
@@ -85,7 +97,6 @@ class DMAIAgent:
85
97
  self._memory[self._validate_memory_id(memory_id)] = []
86
98
 
87
99
  def _prepare_messages_node(self, state: State) -> State:
88
- state.memory_id = self._validate_memory_id(state.memory_id)
89
100
  state.input_messages = state.input_messages or [{"role": "user", "content": ""}]
90
101
  for item in state.input_messages:
91
102
  if isinstance(item, dict):
@@ -102,14 +113,23 @@ class DMAIAgent:
102
113
  state.messages.append(item)
103
114
 
104
115
  if self._input_output_logging:
105
- self._logger.debug(f"Query:\n{state.messages[-1].content}", memory_id=state.memory_id)
116
+ log_kwargs = {} if state.memory_id is None else {"memory_id": state.memory_id}
117
+ self._logger.debug(f"Query:\n{state.messages[-1].content}", **log_kwargs)
106
118
  if self._is_memory_enabled:
107
119
  state.messages = self.get_memory_messages(state.memory_id) + state.messages
108
120
  return state
109
121
 
110
- def _invoke_llm_node(self, state: State) -> State:
122
+ def _invoke_llm_node(self, state: State, second_attempt: bool = False) -> State:
111
123
  self._logger.debug("Run node: Invoke LLM")
112
- ai_response = self._agent.invoke({"messages": state.messages})
124
+ try:
125
+ ai_response = self._agent.invoke({"messages": state.messages})
126
+ except Exception as e:
127
+ self._logger.error(e)
128
+ if second_attempt:
129
+ response = self._response_if_invalid_image if "invalid_image_url" in str(e) else self._response_if_request_fail
130
+ state.messages.append(AIMessage(content=response))
131
+ return state
132
+ return self._invoke_llm_node(state, second_attempt=True)
113
133
  state.messages.append(ai_response)
114
134
  return state
115
135
 
@@ -148,12 +168,21 @@ class DMAIAgent:
148
168
  def _exit_node(self, state: State) -> State:
149
169
  answer = state.messages[-1].content
150
170
  if self._input_output_logging:
151
- self._logger.debug(f"Answer:\n{answer}", memory_id=state.memory_id)
171
+ log_kwargs = {} if state.memory_id is None else {"memory_id": state.memory_id}
172
+ self._logger.debug(f"Answer:\n{answer}", **log_kwargs)
152
173
 
153
174
  if self._is_memory_enabled:
175
+ memory_id = self._validate_memory_id(state.memory_id)
154
176
  messages_to_memory = state.messages[-self._max_memory_messages:]
155
- # drop ToolsMessages from start of list
156
- self._memory[state.memory_id] = list(dropwhile(lambda x: isinstance(x, ToolMessage), messages_to_memory))
177
+ if self._save_tools_responses_in_memory:
178
+ # drop ToolsMessages from start of list
179
+ self._memory[memory_id] = list(dropwhile(lambda x: isinstance(x, ToolMessage), messages_to_memory))
180
+ else:
181
+ self._memory[memory_id] = []
182
+ for mes in messages_to_memory:
183
+ if isinstance(mes, ToolMessage) or (isinstance(mes, AIMessage) and mes.tool_calls):
184
+ continue
185
+ self._memory[memory_id].append(mes)
157
186
  state.response = answer
158
187
  else:
159
188
  state.response = state.messages[len(state.input_messages):]
@@ -1,6 +1,6 @@
1
1
  import sys
2
2
  import asyncio
3
- from langchain_core.messages import ToolMessage
3
+ from langchain_core.messages import AIMessage, ToolMessage
4
4
 
5
5
  from .ai_agent import DMAIAgent
6
6
  from .types import *
@@ -18,9 +18,17 @@ class DMAioAIAgent(DMAIAgent):
18
18
  state = await self._graph.ainvoke({"input_messages": input_messages, "memory_id": memory_id})
19
19
  return state["response"]
20
20
 
21
- async def _invoke_llm_node(self, state: State) -> State:
21
+ async def _invoke_llm_node(self, state: State, second_attempt: bool = False) -> State:
22
22
  self._logger.debug("Run node: Invoke LLM")
23
- ai_response = await self._agent.ainvoke({"messages": state.messages})
23
+ try:
24
+ ai_response = await self._agent.ainvoke({"messages": state.messages})
25
+ except Exception as e:
26
+ self._logger.error(e)
27
+ if second_attempt:
28
+ response = self._response_if_invalid_image if "invalid_image_url" in str(e) else self._response_if_request_fail
29
+ state.messages.append(AIMessage(content=response))
30
+ return state
31
+ return await self._invoke_llm_node(state, second_attempt=True)
24
32
  state.messages.append(ai_response)
25
33
  return state
26
34
 
@@ -0,0 +1,15 @@
1
+ class ImageMessageContentBuilder(list):
2
+ def __init__(self, image_url: str, text: str = None):
3
+ content = []
4
+ if isinstance(text, str):
5
+ content.append({
6
+ "type": "text",
7
+ "text": text
8
+ })
9
+ content.append({
10
+ "type": "image_url",
11
+ "image_url": {
12
+ "url": image_url
13
+ }
14
+ })
15
+ super().__init__(content)
dm_aioaiagent/types.py CHANGED
@@ -4,12 +4,33 @@ from pydantic import BaseModel, Field
4
4
  from langchain_core.messages import BaseMessage
5
5
 
6
6
 
7
- class Message(TypedDict):
7
+ class ImageMessageTextMessage(TypedDict):
8
+ type: Literal['text']
9
+ text: str
10
+
11
+
12
+ class ImageMessageImageItem(TypedDict):
13
+ type: Literal['image_url']
14
+ image_url: dict
15
+
16
+
17
+ ImageMessageContent = list[Union[ImageMessageTextMessage, ImageMessageImageItem]]
18
+
19
+
20
+ class ImageMessage(TypedDict):
21
+ role: Literal["user"]
22
+ content: ImageMessageContent
23
+
24
+
25
+ class TextMessage(TypedDict):
8
26
  role: Literal["user", "ai"]
9
27
  content: str
10
28
 
11
29
 
30
+ Message = Union[TextMessage, ImageMessage]
31
+
12
32
  InputMessagesType = list[Union[Message, BaseMessage]]
33
+
13
34
  ResponseType = Union[str, list[BaseMessage]]
14
35
 
15
36
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dm-aioaiagent
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: This is my custom aioaiagent client
5
5
  Home-page: https://pypi.org/project/dm-aioaiagent
6
6
  Author: dimka4621
@@ -18,6 +18,7 @@ Requires-Dist: pydantic<3.0.0,>=2.9.2
18
18
  Requires-Dist: langchain~=0.3.0
19
19
  Requires-Dist: langchain-core~=0.3.5
20
20
  Requires-Dist: langgraph~=0.2.23
21
+ Requires-Dist: grandalf>=0.8
21
22
  Requires-Dist: langchain-community~=0.3.0
22
23
  Requires-Dist: langchain-openai~=0.2.0
23
24
 
@@ -137,6 +138,34 @@ if __name__ == "__main__":
137
138
  asyncio.run(main())
138
139
  ```
139
140
 
141
+ ### Image vision
142
+
143
+ ```python
144
+ from dm_aioaiagent import DMAIAgent, ImageMessageContentBuilder
145
+
146
+ def main():
147
+ # create an agent
148
+ ai_agent = DMAIAgent(agent_name="image_vision", model="gpt-4o")
149
+
150
+ # create an image message content
151
+ # NOTE: text argument is optional
152
+ img_content = ImageMessageContentBuilder(image_url="https://your.domain/image",
153
+ text="Hello, what is shown in the photo?")
154
+
155
+ # define the conversation message
156
+ messages = [
157
+ {"role": "user", "content": "Hello!"},
158
+ {"role": "user", "content": img_content},
159
+ ]
160
+
161
+ # call an agent
162
+ answer = ai_agent.run(messages)
163
+
164
+
165
+ if __name__ == "__main__":
166
+ main()
167
+ ```
168
+
140
169
  ### Set custom logger
141
170
 
142
171
  _If you want set up custom logger_
@@ -0,0 +1,9 @@
1
+ dm_aioaiagent/__init__.py,sha256=pq9gL6E1VN4Tkx9PD83AIW9e_-N5REqJeC0TE8bZua4,215
2
+ dm_aioaiagent/ai_agent.py,sha256=DY4SVdexuCSeS3P2iCm3dmi7UsZ5dEZ0ufd38jam03Q,9997
3
+ dm_aioaiagent/async_ai_agent.py,sha256=qg7LZRxC7MSzjjburEX09T5EF15ZXAWAd_9RB8vA0oE,2599
4
+ dm_aioaiagent/image_message_content_builder.py,sha256=mAJnsWdnpYpaFAdcCsVwgWH480Q4WfVEpM-Y6KdjyEc,435
5
+ dm_aioaiagent/types.py,sha256=H2_iICmWr6u9d1-6BtSxdt6qV6Jm8v7Zh8das5kV6I4,989
6
+ dm_aioaiagent-0.3.4.dist-info/METADATA,sha256=F_yZ1zenPup4UqUmDJD4cnE-I7U8MoI9z90gNO2Kces,5032
7
+ dm_aioaiagent-0.3.4.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
8
+ dm_aioaiagent-0.3.4.dist-info/top_level.txt,sha256=CbasLH0KI7zA77XwT6JDCnmRascxKNGvUVV9MgYjHAU,14
9
+ dm_aioaiagent-0.3.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.2.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,8 +0,0 @@
1
- dm_aioaiagent/__init__.py,sha256=8B0XQR-XE2VieU7-LOHCjqDYv0gSYqkgAkj_eq6XAhI,145
2
- dm_aioaiagent/ai_agent.py,sha256=2IcRBAXsp8JJQ8otNnJjeLwzYZK9vlKa6rbUv-F1WjQ,8213
3
- dm_aioaiagent/async_ai_agent.py,sha256=n7OfJUHRwjk92gae9NkpPKQ21Xost2-JsQ0JyzYTXT4,2146
4
- dm_aioaiagent/types.py,sha256=Xvx0x1GxLAvCot_3CZGgD8BFWwnTEc1lg7moLxkkJoc,587
5
- dm_aioaiagent-0.3.2.dist-info/METADATA,sha256=WNAzIo7kKPRsPFxVeSPWSzSgn2VOeLCkEV3HRZulU4w,4299
6
- dm_aioaiagent-0.3.2.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
7
- dm_aioaiagent-0.3.2.dist-info/top_level.txt,sha256=CbasLH0KI7zA77XwT6JDCnmRascxKNGvUVV9MgYjHAU,14
8
- dm_aioaiagent-0.3.2.dist-info/RECORD,,