llm-dialog-manager 0.4.7__tar.gz → 0.5.0__tar.gz

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.
Files changed (20) hide show
  1. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/PKG-INFO +2 -2
  2. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/README.md +1 -1
  3. llm_dialog_manager-0.5.0/llm_dialog_manager/__init__.py +20 -0
  4. llm_dialog_manager-0.5.0/llm_dialog_manager/agent.py +189 -0
  5. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/llm_dialog_manager.egg-info/PKG-INFO +2 -2
  6. llm_dialog_manager-0.5.0/pyproject.toml +64 -0
  7. llm_dialog_manager-0.4.7/llm_dialog_manager/__init__.py +0 -4
  8. llm_dialog_manager-0.4.7/llm_dialog_manager/agent.py +0 -642
  9. llm_dialog_manager-0.4.7/pyproject.toml +0 -32
  10. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/LICENSE +0 -0
  11. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/llm_dialog_manager/chat_history.py +0 -0
  12. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/llm_dialog_manager/key_manager.py +0 -0
  13. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/llm_dialog_manager.egg-info/SOURCES.txt +0 -0
  14. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/llm_dialog_manager.egg-info/dependency_links.txt +0 -0
  15. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/llm_dialog_manager.egg-info/requires.txt +0 -0
  16. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/llm_dialog_manager.egg-info/top_level.txt +0 -0
  17. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/setup.cfg +0 -0
  18. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/tests/test_agent.py +0 -0
  19. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/tests/test_chat_history.py +0 -0
  20. {llm_dialog_manager-0.4.7 → llm_dialog_manager-0.5.0}/tests/test_key_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llm_dialog_manager
3
- Version: 0.4.7
3
+ Version: 0.5.0
4
4
  Summary: A Python package for managing LLM chat conversation history
5
5
  Author-email: xihajun <work@2333.fun>
6
6
  License: MIT
@@ -103,7 +103,7 @@ XAI_API_KEY=your-x-key
103
103
  from llm_dialog_manager import Agent
104
104
 
105
105
  # Initialize an agent with a specific model
106
- agent = Agent("claude-2.1", memory_enabled=True)
106
+ agent = Agent("ep-20250319212209-j6tfj-openai", memory_enabled=True)
107
107
 
108
108
  # Add messages and generate responses
109
109
  agent.add_message("system", "You are a helpful assistant")
@@ -55,7 +55,7 @@ XAI_API_KEY=your-x-key
55
55
  from llm_dialog_manager import Agent
56
56
 
57
57
  # Initialize an agent with a specific model
58
- agent = Agent("claude-2.1", memory_enabled=True)
58
+ agent = Agent("ep-20250319212209-j6tfj-openai", memory_enabled=True)
59
59
 
60
60
  # Add messages and generate responses
61
61
  agent.add_message("system", "You are a helpful assistant")
@@ -0,0 +1,20 @@
1
+ """
2
+ LLM Dialog Manager
3
+
4
+ A modular framework for building conversational AI applications with
5
+ support for multiple LLM providers.
6
+ """
7
+
8
+ __version__ = "0.5.0"
9
+
10
+ from .agent import Agent
11
+ from .chat_history import ChatHistory
12
+ from .key_manager import key_manager
13
+
14
+ # Import factory functions for easy access
15
+ from .clients import get_client
16
+ from .formatters import get_formatter
17
+
18
+ # Setup environment by default
19
+ from .utils.environment import load_env_vars
20
+ load_env_vars()
@@ -0,0 +1,189 @@
1
+ """
2
+ Agent class for managing LLM conversations
3
+ """
4
+ # Standard library imports
5
+ import uuid
6
+ import logging
7
+ from typing import List, Dict, Optional, Union
8
+ from PIL import Image
9
+
10
+ # Local imports
11
+ from .chat_history import ChatHistory
12
+ from .clients import get_client
13
+ from .utils.environment import load_env_vars
14
+ from .utils.image_tools import load_image_from_path, load_image_from_url, create_image_content_block
15
+
16
+ # Setup logging
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Load environment variables
20
+ load_env_vars()
21
+
22
+ class Agent:
23
+ """
24
+ Agent class for managing conversations with LLMs.
25
+
26
+ This class provides a high-level interface for interacting with different
27
+ LLM providers through a unified API.
28
+ """
29
+
30
+ def __init__(self, model_name: str,
31
+ messages: Optional[Union[str, List[Dict[str, Union[str, List[Union[str, Image.Image, Dict]]]]]]] = None,
32
+ memory_enabled: bool = False,
33
+ api_key: Optional[str] = None,
34
+ base_url: Optional[str] = None) -> None:
35
+ """
36
+ Initialize an Agent instance.
37
+
38
+ Args:
39
+ model_name: Name of the LLM model to use
40
+ messages: Optional initial messages or system prompt
41
+ memory_enabled: Whether to enable conversation memory
42
+ api_key: Optional API key to use
43
+ base_url: Optional base URL for API requests
44
+ """
45
+ self.id = f"{model_name}-{uuid.uuid4().hex[:8]}"
46
+ self.model_name = model_name
47
+ self.history = ChatHistory(messages) if messages else ChatHistory()
48
+ self.memory_enabled = memory_enabled
49
+ self.client = get_client(model_name, api_key=api_key, base_url=base_url)
50
+ self.repo_content = []
51
+
52
+ def add_message(self, role: str, content: Union[str, List[Union[str, Image.Image, Dict]]]):
53
+ """
54
+ Add a message to the conversation.
55
+
56
+ Args:
57
+ role: Message role ('system', 'user', or 'assistant')
58
+ content: Message content (text, image, or mixed content)
59
+ """
60
+ self.history.add_message(content, role)
61
+
62
+ def add_user_message(self, content: Union[str, List[Union[str, Image.Image, Dict]]]):
63
+ """
64
+ Add a user message to the conversation.
65
+
66
+ Args:
67
+ content: Message content (text, image, or mixed content)
68
+ """
69
+ self.history.add_user_message(content)
70
+
71
+ def add_assistant_message(self, content: Union[str, List[Union[str, Image.Image, Dict]]]):
72
+ """
73
+ Add an assistant message to the conversation.
74
+
75
+ Args:
76
+ content: Message content (text, image, or mixed content)
77
+ """
78
+ self.history.add_assistant_message(content)
79
+
80
+ def add_image(self, image_path: Optional[str] = None,
81
+ image_url: Optional[str] = None,
82
+ media_type: Optional[str] = "image/jpeg"):
83
+ """
84
+ Add an image to the conversation.
85
+
86
+ Either image_path or image_url must be provided.
87
+
88
+ Args:
89
+ image_path: Path to a local image file
90
+ image_url: URL of an image
91
+ media_type: MIME type of the image
92
+
93
+ Returns:
94
+ The image content block that was added
95
+ """
96
+ if not (image_path or image_url):
97
+ raise ValueError("Either image_path or image_url must be provided.")
98
+
99
+ if image_path:
100
+ image = load_image_from_path(image_path)
101
+ else:
102
+ image = load_image_from_url(image_url)
103
+
104
+ return create_image_content_block(image, media_type)
105
+
106
+ def generate_response(self, max_tokens=3585, temperature=0.7,
107
+ top_p=1.0, top_k=40, json_format=False, **kwargs):
108
+ """
109
+ Generate a response from the agent.
110
+
111
+ Args:
112
+ max_tokens: Maximum number of tokens to generate
113
+ temperature: Sampling temperature
114
+ top_p: Nucleus sampling parameter
115
+ top_k: Top-k sampling parameter
116
+ json_format: Whether to enable JSON output format
117
+ **kwargs: Additional model-specific parameters
118
+
119
+ Returns:
120
+ The generated response text
121
+ """
122
+ response = self.client.completion(
123
+ messages=self.history.messages,
124
+ max_tokens=max_tokens,
125
+ temperature=temperature,
126
+ top_p=top_p,
127
+ top_k=top_k,
128
+ json_format=json_format,
129
+ model=self.model_name,
130
+ **kwargs
131
+ )
132
+
133
+ # Add the response to history
134
+ if not json_format:
135
+ self.add_assistant_message(response)
136
+
137
+ return response
138
+
139
+ def save_conversation(self, filename=None):
140
+ """
141
+ Save the conversation history to a file.
142
+
143
+ Args:
144
+ filename: Optional filename to save to
145
+ """
146
+ if filename is None:
147
+ filename = f"conversation_{self.id}.json"
148
+
149
+ import json
150
+
151
+ # Convert any PIL.Image objects to base64 for serialization
152
+ serializable_history = []
153
+ for msg in self.history.messages:
154
+ role = msg["role"]
155
+ content = msg["content"]
156
+
157
+ if isinstance(content, str):
158
+ serializable_history.append({"role": role, "content": content})
159
+ elif isinstance(content, list):
160
+ serializable_content = []
161
+ for item in content:
162
+ if isinstance(item, str):
163
+ serializable_content.append(item)
164
+ elif isinstance(item, Image.Image):
165
+ serializable_content.append(create_image_content_block(item))
166
+ elif isinstance(item, dict):
167
+ serializable_content.append(item)
168
+ serializable_history.append({"role": role, "content": serializable_content})
169
+
170
+ with open(filename, 'w') as f:
171
+ json.dump(serializable_history, f, indent=2)
172
+
173
+ return filename
174
+
175
+ def load_conversation(self, filename):
176
+ """
177
+ Load a conversation from a file.
178
+
179
+ Args:
180
+ filename: Path to the conversation file
181
+ """
182
+ import json
183
+
184
+ with open(filename, 'r') as f:
185
+ history = json.load(f)
186
+
187
+ self.history = ChatHistory(history)
188
+
189
+ return self.history
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llm_dialog_manager
3
- Version: 0.4.7
3
+ Version: 0.5.0
4
4
  Summary: A Python package for managing LLM chat conversation history
5
5
  Author-email: xihajun <work@2333.fun>
6
6
  License: MIT
@@ -103,7 +103,7 @@ XAI_API_KEY=your-x-key
103
103
  from llm_dialog_manager import Agent
104
104
 
105
105
  # Initialize an agent with a specific model
106
- agent = Agent("claude-2.1", memory_enabled=True)
106
+ agent = Agent("ep-20250319212209-j6tfj-openai", memory_enabled=True)
107
107
 
108
108
  # Add messages and generate responses
109
109
  agent.add_message("system", "You are a helpful assistant")
@@ -0,0 +1,64 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "llm_dialog_manager"
7
+ version = "0.5.0"
8
+ description = "A Python package for managing LLM chat conversation history"
9
+ readme = "README.md"
10
+ authors = [{ name = "xihajun", email = "work@2333.fun" }]
11
+ license = { text = "MIT" }
12
+ classifiers = [
13
+ "Development Status :: 3 - Alpha",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3.8",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
22
+ ]
23
+ requires-python = ">=3.7"
24
+ dependencies = [
25
+ "openai>=1.54.2",
26
+ "anthropic>=0.39.0",
27
+ "google-generativeai>=0.1.0",
28
+ "python-dotenv>=1.0.0",
29
+ "typing-extensions>=4.0.0",
30
+ "uuid>=1.30",
31
+ ]
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=8.0.0",
36
+ "pytest-asyncio>=0.21.1",
37
+ "pytest-cov>=4.1.0",
38
+ "black>=23.9.1",
39
+ "isort>=5.12.0",
40
+ ]
41
+ test = [
42
+ "pytest>=6.0",
43
+ "pytest-asyncio>=0.14.0",
44
+ "pytest-cov>=2.0",
45
+ ]
46
+ lint = [
47
+ "black>=22.0",
48
+ "isort>=5.0",
49
+ ]
50
+ all = [
51
+ "pytest>=8.0.0",
52
+ "pytest-asyncio>=0.21.1",
53
+ "pytest-cov>=4.1.0",
54
+ "black>=23.9.1",
55
+ "isort>=5.12.0",
56
+ ]
57
+
58
+ [project.urls]
59
+ "Bug Tracker" = "https://github.com/xihajun/llm_dialog_manager/issues"
60
+ "Documentation" = "https://github.com/xihajun/llm_dialog_manager#readme"
61
+ "Source Code" = "https://github.com/xihajun/llm_dialog_manager"
62
+
63
+ [tool.setuptools]
64
+ packages = ["llm_dialog_manager"]
@@ -1,4 +0,0 @@
1
- from .chat_history import ChatHistory
2
- from .agent import Agent
3
-
4
- __version__ = "0.4.7"
@@ -1,642 +0,0 @@
1
- # Standard library imports
2
- import json
3
- import os
4
- import uuid
5
- from typing import List, Dict, Union, Optional, Any
6
- import logging
7
- from pathlib import Path
8
- import random
9
- import requests
10
- import zipfile
11
- import io
12
- import base64
13
- from PIL import Image
14
-
15
- # Third-party imports
16
- import anthropic
17
- from anthropic import AnthropicVertex
18
- import google.generativeai as genai
19
- import openai
20
- from dotenv import load_dotenv
21
-
22
- # Local imports
23
- from .chat_history import ChatHistory
24
- from .key_manager import key_manager
25
-
26
- # Set up logging
27
- logging.basicConfig(level=logging.INFO)
28
- logger = logging.getLogger(__name__)
29
-
30
- # Load environment variables
31
- def load_env_vars():
32
- """Load environment variables from .env file"""
33
- env_path = Path(__file__).parent / '.env'
34
- if env_path.exists():
35
- load_dotenv(env_path)
36
- else:
37
- logger.warning(".env file not found. Using system environment variables.")
38
-
39
- load_env_vars()
40
-
41
- def encode_image(image_path):
42
- with open(image_path, "rb") as image_file:
43
- return base64.b64encode(image_file.read()).decode("utf-8")
44
-
45
- def format_messages_for_gemini(messages):
46
- """
47
- 将标准化的消息格式转化为 Gemini 格式。
48
- system 消息应该通过 GenerativeModel 的 system_instruction 参数传入,
49
- 不在这个函数处理。
50
- """
51
- gemini_messages = []
52
-
53
- for msg in messages:
54
- role = msg["role"]
55
- content = msg["content"]
56
-
57
- # 跳过 system 消息,因为它会通过 system_instruction 设置
58
- if role == "system":
59
- continue
60
-
61
- # 处理 user/assistant 消息
62
- # 如果 content 是单一对象,转换为列表
63
- if not isinstance(content, list):
64
- content = [content]
65
-
66
- gemini_messages.append({
67
- "role": role,
68
- "parts": content # content 可以包含文本和 FileMedia
69
- })
70
-
71
- return gemini_messages
72
-
73
- def completion(model: str, messages: List[Dict[str, Union[str, List[Union[str, Image.Image, Dict]]]]], max_tokens: int = 1000,
74
- temperature: float = 0.5, top_p: float = 1.0, top_k: int = 40, api_key: Optional[str] = None,
75
- base_url: Optional[str] = None, json_format: bool = False) -> str:
76
- """
77
- Generate a completion using the specified model and messages.
78
- """
79
- try:
80
- service = ""
81
- if "openai" in model:
82
- service = "openai"
83
- model
84
- elif "claude" in model:
85
- service = "anthropic"
86
- elif "gemini" in model:
87
- service = "gemini"
88
- elif "grok" in model:
89
- service = "x"
90
- else:
91
- service = "openai"
92
-
93
- # Get API key and base URL from key manager if not provided
94
- if not api_key:
95
- # api_key, base_url = key_manager.get_config(service)
96
- # Placeholder for key_manager
97
- api_key = os.getenv(f"{service.upper()}_API_KEY")
98
- base_url = os.getenv(f"{service.upper()}_BASE_URL")
99
-
100
- def format_messages_for_api(
101
- model: str,
102
- messages: List[Dict[str, Union[str, List[Union[str, Image.Image, Dict]]]]]
103
- ) -> tuple[Optional[str], List[Dict[str, Any]]]:
104
- """
105
- Convert ChatHistory messages to the format required by the specific API.
106
-
107
- Args:
108
- model: The model name (e.g., "claude", "gemini", "gpt")
109
- messages: List of message dictionaries with role and content
110
-
111
- Returns:
112
- tuple: (system_message, formatted_messages)
113
- - system_message is extracted system message for Claude, None for others
114
- - formatted_messages is the list of formatted message dictionaries
115
- """
116
- if "claude" in model and "openai" not in model:
117
- formatted = []
118
- system_msg = ""
119
-
120
- # Extract system message if present
121
- if messages and messages[0]["role"] == "system":
122
- system_msg = messages.pop(0)["content"]
123
-
124
- for msg in messages:
125
- content = msg["content"]
126
- if isinstance(content, str):
127
- formatted.append({"role": msg["role"], "content": content})
128
- elif isinstance(content, list):
129
- # Combine content blocks into a single message
130
- combined_content = []
131
- for block in content:
132
- if isinstance(block, str):
133
- combined_content.append({
134
- "type": "text",
135
- "text": block
136
- })
137
- elif isinstance(block, Image.Image):
138
- # Convert PIL.Image to base64
139
- buffered = io.BytesIO()
140
- block.save(buffered, format="PNG")
141
- image_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
142
- combined_content.append({
143
- "type": "image",
144
- "source": {
145
- "type": "base64",
146
- "media_type": "image/png",
147
- "data": image_base64
148
- }
149
- })
150
- elif isinstance(block, dict):
151
- if block.get("type") == "image_url":
152
- combined_content.append({
153
- "type": "image",
154
- "source": {
155
- "type": "url",
156
- "url": block["image_url"]["url"]
157
- }
158
- })
159
- elif block.get("type") == "image_base64":
160
- combined_content.append({
161
- "type": "image",
162
- "source": {
163
- "type": "base64",
164
- "media_type": block["image_base64"]["media_type"],
165
- "data": block["image_base64"]["data"]
166
- }
167
- })
168
- formatted.append({
169
- "role": msg["role"],
170
- "content": combined_content
171
- })
172
- return system_msg, formatted
173
-
174
- elif ("gemini" in model or "gpt" in model or "grok" in model) and "openai" not in model:
175
- formatted = []
176
- for msg in messages:
177
- content = msg["content"]
178
- if isinstance(content, str):
179
- formatted.append({"role": msg["role"], "parts": [content]})
180
- elif isinstance(content, list):
181
- parts = []
182
- for block in content:
183
- if isinstance(block, str):
184
- parts.append(block)
185
- elif isinstance(block, Image.Image):
186
- # Keep PIL.Image objects as is for Gemini
187
- parts.append(block)
188
- elif isinstance(block, dict):
189
- if block.get("type") == "image_url":
190
- parts.append({
191
- "type": "image_url",
192
- "image_url": {
193
- "url": block["image_url"]["url"]
194
- }
195
- })
196
- elif block.get("type") == "image_base64":
197
- parts.append({
198
- "type": "image_base64",
199
- "image_base64": {
200
- "data": block["image_base64"]["data"],
201
- "media_type": block["image_base64"]["media_type"]
202
- }
203
- })
204
- formatted.append({
205
- "role": msg["role"],
206
- "parts": parts
207
- })
208
- return None, formatted
209
-
210
- else: # OpenAI models
211
- formatted = []
212
- for msg in messages:
213
- content = msg["content"]
214
- if isinstance(content, str):
215
- formatted.append({
216
- "role": msg["role"],
217
- "content": content
218
- })
219
- elif isinstance(content, list):
220
- formatted_content = []
221
- for block in content:
222
- if isinstance(block, str):
223
- formatted_content.append({
224
- "type": "text",
225
- "text": block
226
- })
227
- elif isinstance(block, Image.Image):
228
- # Convert PIL.Image to base64
229
- buffered = io.BytesIO()
230
- block.save(buffered, format="PNG")
231
- image_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
232
- formatted_content.append({
233
- "type": "image_url",
234
- "image_url": {
235
- "url": f"data:image/jpeg;base64,{image_base64}"
236
- }
237
- })
238
- elif isinstance(block, dict):
239
- if block.get("type") == "image_url":
240
- formatted_content.append({
241
- "type": "image_url",
242
- "image_url": block["image_url"]
243
- })
244
- elif block.get("type") == "image_base64":
245
- formatted_content.append({
246
- "type": "image_url",
247
- "image_url": {
248
- "url": f"data:image/jpeg;base64,{block['image_base64']['data']}"
249
- }
250
- })
251
- formatted.append({
252
- "role": msg["role"],
253
- "content": formatted_content
254
- })
255
- return None, formatted
256
-
257
- system_msg, formatted_messages = format_messages_for_api(model, messages.copy())
258
-
259
- if "claude" in model and "openai" not in model:
260
- # Check for Vertex configuration
261
- vertex_project_id = os.getenv('VERTEX_PROJECT_ID')
262
- vertex_region = os.getenv('VERTEX_REGION')
263
-
264
- if vertex_project_id and vertex_region:
265
- client = AnthropicVertex(
266
- region=vertex_region,
267
- project_id=vertex_project_id
268
- )
269
- else:
270
- client = anthropic.Anthropic(api_key=api_key, base_url=base_url)
271
-
272
- response = client.messages.create(
273
- model=model,
274
- max_tokens=max_tokens,
275
- temperature=temperature,
276
- messages=formatted_messages,
277
- system=system_msg
278
- )
279
-
280
- while response.stop_reason == "max_tokens":
281
- if formatted_messages[-1]['role'] == "user":
282
- formatted_messages.append({"role": "assistant", "content": response.completion})
283
- else:
284
- formatted_messages[-1]['content'] += response.completion
285
-
286
- response = client.messages.create(
287
- model=model,
288
- max_tokens=max_tokens,
289
- temperature=temperature,
290
- messages=formatted_messages,
291
- system=system_msg
292
- )
293
-
294
- if formatted_messages[-1]['role'] == "assistant" and response.stop_reason == "end_turn":
295
- formatted_messages[-1]['content'] += response.completion
296
- return formatted_messages[-1]['content']
297
-
298
- return response.completion
299
-
300
- elif "gemini" in model and "openai" not in model:
301
- try:
302
- # First try OpenAI-style API
303
- client = openai.OpenAI(
304
- api_key=api_key,
305
- base_url="https://generativelanguage.googleapis.com/v1beta/"
306
- )
307
- # Set response_format based on json_format
308
- response_format = {"type": "json_object"} if json_format else {"type": "plain_text"}
309
-
310
- response = client.chat.completions.create(
311
- model=model,
312
- max_tokens=max_tokens,
313
- top_p=top_p,
314
- top_k=top_k,
315
- messages=formatted_messages,
316
- temperature=temperature,
317
- response_format=response_format # Added response_format
318
- )
319
- return response.choices[0].message.content
320
-
321
- except Exception as e:
322
- # If OpenAI-style API fails, fall back to Google's genai library
323
- logger.info("Falling back to Google's genai library")
324
- genai.configure(api_key=api_key)
325
- system_instruction = ""
326
- for msg in messages:
327
- if msg["role"] == "system":
328
- system_instruction = msg["content"]
329
- break
330
-
331
- # 将其他消息转换为 gemini 格式
332
- gemini_messages = format_messages_for_gemini(messages)
333
- mime_type = "application/json" if json_format else "text/plain"
334
- generation_config = genai.types.GenerationConfig(
335
- temperature=temperature,
336
- top_p=top_p,
337
- top_k=top_k,
338
- max_output_tokens=max_tokens,
339
- response_mime_type=mime_type
340
- )
341
-
342
- model_instance = genai.GenerativeModel(
343
- model_name=model,
344
- system_instruction=system_instruction, # system 消息通过这里传入
345
- generation_config=generation_config
346
- )
347
-
348
- response = model_instance.generate_content(gemini_messages, generation_config=generation_config)
349
-
350
- return response.text
351
-
352
- elif "grok" in model and "openai" not in model:
353
- # Randomly choose between OpenAI and Anthropic SDK
354
- use_anthropic = random.choice([True, False])
355
-
356
- if use_anthropic:
357
- logger.info("Using Anthropic for Grok model")
358
- client = anthropic.Anthropic(
359
- api_key=api_key,
360
- base_url="https://api.x.ai"
361
- )
362
-
363
- system_msg = ""
364
- if messages and messages[0]["role"] == "system":
365
- system_msg = messages.pop(0)["content"]
366
-
367
- response = client.messages.create(
368
- model=model,
369
- max_tokens=max_tokens,
370
- temperature=temperature,
371
- messages=formatted_messages,
372
- system=system_msg
373
- )
374
- return response.completion
375
- else:
376
- logger.info("Using OpenAI for Grok model")
377
- client = openai.OpenAI(
378
- api_key=api_key,
379
- base_url="https://api.x.ai/v1"
380
- )
381
- # Set response_format based on json_format
382
- response_format = {"type": "json_object"} if json_format else {"type": "plain_text"}
383
-
384
- response = client.chat.completions.create(
385
- model=model,
386
- messages=formatted_messages,
387
- max_tokens=max_tokens,
388
- temperature=temperature,
389
- response_format=response_format # Added response_format
390
- )
391
- return response.choices[0].message.content
392
-
393
- else: # OpenAI models
394
- if model.endswith("-openai"):
395
- model = model[:-7] # Remove last 7 characters ("-openai")
396
-
397
- # Initialize OpenAI client with only supported parameters
398
- client_kwargs = {"api_key": api_key}
399
- if base_url:
400
- client_kwargs["base_url"] = base_url
401
- client = openai.OpenAI(**client_kwargs)
402
-
403
- # Create base parameters
404
- params = {
405
- "model": model,
406
- "messages": formatted_messages,
407
- }
408
-
409
- # Add optional parameters
410
- if json_format:
411
- params["response_format"] = {"type": "json_object"}
412
- if not ("o1" in model or "o3" in model):
413
- params["max_tokens"] = max_tokens
414
- params["temperature"] = temperature
415
-
416
- response = client.chat.completions.create(**params)
417
- return response.choices[0].message.content
418
-
419
- # Release the API key after successful use
420
- if not api_key:
421
- # key_manager.release_config(service, api_key)
422
- pass
423
-
424
- return response
425
-
426
- except Exception as e:
427
- logger.error(f"Error in completion: {str(e)}")
428
- raise
429
-
430
- class Agent:
431
- def __init__(self, model_name: str, messages: Optional[Union[str, List[Dict[str, Union[str, List[Union[str, Image.Image, Dict]]]]]]] = None,
432
- memory_enabled: bool = False, api_key: Optional[str] = None) -> None:
433
- """Initialize an Agent instance."""
434
- self.id = f"{model_name}-{uuid.uuid4().hex[:8]}"
435
- self.model_name = model_name
436
- self.history = ChatHistory(messages) if messages else ChatHistory()
437
- self.memory_enabled = memory_enabled
438
- self.api_key = api_key
439
- self.repo_content = []
440
-
441
- def add_message(self, role: str, content: Union[str, List[Union[str, Image.Image, Dict]]]):
442
- """Add a message to the conversation."""
443
- self.history.add_message(content, role)
444
-
445
- def add_user_message(self, content: Union[str, List[Union[str, Image.Image, Dict]]]):
446
- """Add a user message."""
447
- self.history.add_user_message(content)
448
-
449
- def add_assistant_message(self, content: Union[str, List[Union[str, Image.Image, Dict]]]):
450
- """Add an assistant message."""
451
- self.history.add_assistant_message(content)
452
-
453
- def add_image(self, image_path: Optional[str] = None, image_url: Optional[str] = None, media_type: Optional[str] = "image/jpeg"):
454
- """
455
- Add an image to the conversation.
456
- Either image_path or image_url must be provided.
457
- """
458
- if not image_path and not image_url:
459
- raise ValueError("Either image_path or image_url must be provided.")
460
-
461
- if image_path:
462
- if not os.path.exists(image_path):
463
- raise FileNotFoundError(f"Image file {image_path} does not exist.")
464
- if "gemini" in self.model_name and "openai" not in self.model_name:
465
- # For Gemini, load as PIL.Image
466
- image_pil = Image.open(image_path)
467
- image_block = image_pil
468
- elif "claude" in self.model_name and "openai" not in self.model_name:
469
- # For Claude and others, use base64 encoding
470
- with open(image_path, "rb") as img_file:
471
- image_data = base64.standard_b64encode(img_file.read()).decode("utf-8")
472
- image_block = {
473
- "type": "image",
474
- "source": {
475
- "type": "base64",
476
- "media_type": media_type,
477
- "data": image_data,
478
- },
479
- }
480
- else:
481
- # openai format
482
- base64_image = encode_image(image_path)
483
- image_block = {
484
- "type": "image_url",
485
- "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"},
486
- }
487
- else:
488
- # If image_url is provided
489
- if "gemini" in self.model_name and "openai" not in self.model_name:
490
- # For Gemini, you can pass image URLs directly
491
- image_block = {"type": "image_url", "image_url": {"url": image_url}}
492
- elif "claude" in self.model_name and "openai" not in self.model_name:
493
- import httpx
494
- media_type = "image/jpeg"
495
- image_data = base64.standard_b64encode(httpx.get(image_url).content).decode("utf-8")
496
- image_block = {
497
- "type": "image",
498
- "source": {
499
- "type": "base64",
500
- "media_type": media_type,
501
- "data": image_data,
502
- },
503
- }
504
- else:
505
- # For Claude and others, use image URLs
506
- image_block = {
507
- "type": "image_url",
508
- "image_url": {
509
- "url": image_url
510
- }
511
- }
512
-
513
- # Add the image block to the last user message or as a new user message
514
- if self.history.last_role == "user":
515
- current_content = self.history.messages[-1]["content"]
516
- if isinstance(current_content, list):
517
- current_content.append(image_block)
518
- else:
519
- self.history.messages[-1]["content"] = [current_content, image_block]
520
- else:
521
- # Start a new user message with the image
522
- self.history.add_message([image_block], "user")
523
-
524
- def generate_response(self, max_tokens=3585, temperature=0.7, top_p=1.0, top_k=40, json_format: bool = False) -> str:
525
- """Generate a response from the agent.
526
-
527
- Args:
528
- max_tokens (int, optional): Maximum number of tokens. Defaults to 3585.
529
- temperature (float, optional): Sampling temperature. Defaults to 0.7.
530
- json_format (bool, optional): Whether to enable JSON output format. Defaults to False.
531
-
532
- Returns:
533
- str: The generated response.
534
- """
535
- if not self.history.messages:
536
- raise ValueError("No messages in history to generate response from")
537
-
538
- messages = self.history.messages
539
- print(self.model_name)
540
- response_text = completion(
541
- model=self.model_name,
542
- messages=messages,
543
- max_tokens=max_tokens,
544
- temperature=temperature,
545
- top_p=top_p,
546
- top_k=top_k,
547
- api_key=self.api_key,
548
- json_format=json_format # Pass json_format to completion
549
- )
550
- if self.model_name.startswith("openai"):
551
- # OpenAI does not support images, so responses are simple strings
552
- if self.history.messages[-1]["role"] == "assistant":
553
- self.history.messages[-1]["content"] = response_text
554
- elif self.memory_enabled:
555
- self.add_message("assistant", response_text)
556
- elif "claude" in self.model_name:
557
- if self.history.messages[-1]["role"] == "assistant":
558
- self.history.messages[-1]["content"] = response_text
559
- elif self.memory_enabled:
560
- self.add_message("assistant", response_text)
561
- elif "gemini" in self.model_name or "grok" in self.model_name:
562
- if self.history.messages[-1]["role"] == "assistant":
563
- if isinstance(self.history.messages[-1]["content"], list):
564
- self.history.messages[-1]["content"].append(response_text)
565
- else:
566
- self.history.messages[-1]["content"] = [self.history.messages[-1]["content"], response_text]
567
- elif self.memory_enabled:
568
- self.add_message("assistant", response_text)
569
- else:
570
- # Handle other models similarly
571
- if self.history.messages[-1]["role"] == "assistant":
572
- self.history.messages[-1]["content"] = response_text
573
- elif self.memory_enabled:
574
- self.add_message("assistant", response_text)
575
-
576
- return response_text
577
-
578
- def save_conversation(self):
579
- filename = f"{self.id}.json"
580
- with open(filename, 'w', encoding='utf-8') as file:
581
- json.dump(self.history.messages, file, ensure_ascii=False, indent=4)
582
-
583
- def load_conversation(self, filename: Optional[str] = None):
584
- if filename is None:
585
- filename = f"{self.id}.json"
586
- with open(filename, 'r', encoding='utf-8') as file:
587
- messages = json.load(file)
588
- # Handle deserialization of images if necessary
589
- self.history = ChatHistory(messages)
590
-
591
- def add_repo(self, repo_url: Optional[str] = None, username: Optional[str] = None, repo_name: Optional[str] = None, commit_hash: Optional[str] = None):
592
- if username and repo_name:
593
- if commit_hash:
594
- repo_url = f"https://github.com/{username}/{repo_name}/archive/{commit_hash}.zip"
595
- else:
596
- repo_url = f"https://github.com/{username}/{repo_name}/archive/refs/heads/main.zip"
597
-
598
- if not repo_url:
599
- raise ValueError("Either repo_url or both username and repo_name must be provided")
600
-
601
- response = requests.get(repo_url)
602
- if response.status_code == 200:
603
- repo_content = ""
604
- with zipfile.ZipFile(io.BytesIO(response.content)) as z:
605
- for file_info in z.infolist():
606
- if not file_info.is_dir() and file_info.filename.endswith(('.py', '.txt')):
607
- with z.open(file_info) as f:
608
- content = f.read().decode('utf-8')
609
- repo_content += f"{file_info.filename}\n```\n{content}\n```\n"
610
- self.repo_content.append(repo_content)
611
- else:
612
- raise ValueError(f"Failed to download repository from {repo_url}")
613
-
614
- if __name__ == "__main__":
615
- # Example Usage
616
- # Create an Agent instance (Gemini model)
617
- agent = Agent("gemini-1.5-flash-openai", "you are Jack101", memory_enabled=True)
618
-
619
- # Add an image
620
- agent.add_image(image_path="example.png")
621
-
622
- # Add a user message
623
- agent.add_message("user", "Who are you? What's in this image?")
624
-
625
- # Generate response with JSON format enabled
626
- try:
627
- response = agent.generate_response(json_format=True) # json_format set to True
628
- print("Response:", response)
629
- except Exception as e:
630
- logger.error(f"Failed to generate response: {e}")
631
-
632
- # Print the entire conversation history
633
- print("Conversation History:")
634
- print(agent.history)
635
-
636
- # Pop the last message
637
- last_message = agent.history.pop()
638
- print("Last Message:", last_message)
639
-
640
- # Generate another response without JSON format
641
- response = agent.generate_response()
642
- print("Response:", response)
@@ -1,32 +0,0 @@
1
- [build-system]
2
- requires = [ "setuptools>=61.0", "wheel",]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "llm_dialog_manager"
7
- version = "0.4.7"
8
- description = "A Python package for managing LLM chat conversation history"
9
- readme = "README.md"
10
- classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: Artificial Intelligence",]
11
- requires-python = ">=3.7"
12
- dependencies = [ "openai>=1.54.2", "anthropic>=0.39.0", "google-generativeai>=0.1.0", "python-dotenv>=1.0.0", "typing-extensions>=4.0.0", "uuid>=1.30",]
13
- [[project.authors]]
14
- name = "xihajun"
15
- email = "work@2333.fun"
16
-
17
- [project.license]
18
- text = "MIT"
19
-
20
- [project.optional-dependencies]
21
- dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.21.1", "pytest-cov>=4.1.0", "black>=23.9.1", "isort>=5.12.0",]
22
- test = [ "pytest>=6.0", "pytest-asyncio>=0.14.0", "pytest-cov>=2.0",]
23
- lint = [ "black>=22.0", "isort>=5.0",]
24
- all = [ "pytest>=8.0.0", "pytest-asyncio>=0.21.1", "pytest-cov>=4.1.0", "black>=23.9.1", "isort>=5.12.0",]
25
-
26
- [project.urls]
27
- "Bug Tracker" = "https://github.com/xihajun/llm_dialog_manager/issues"
28
- Documentation = "https://github.com/xihajun/llm_dialog_manager#readme"
29
- "Source Code" = "https://github.com/xihajun/llm_dialog_manager"
30
-
31
- [tool.setuptools]
32
- packages = [ "llm_dialog_manager",]