powermem 0.1.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.
- powermem/__init__.py +103 -0
- powermem/agent/__init__.py +35 -0
- powermem/agent/abstract/__init__.py +22 -0
- powermem/agent/abstract/collaboration.py +259 -0
- powermem/agent/abstract/context.py +187 -0
- powermem/agent/abstract/manager.py +232 -0
- powermem/agent/abstract/permission.py +217 -0
- powermem/agent/abstract/privacy.py +267 -0
- powermem/agent/abstract/scope.py +199 -0
- powermem/agent/agent.py +791 -0
- powermem/agent/components/__init__.py +18 -0
- powermem/agent/components/collaboration_coordinator.py +645 -0
- powermem/agent/components/permission_controller.py +586 -0
- powermem/agent/components/privacy_protector.py +767 -0
- powermem/agent/components/scope_controller.py +685 -0
- powermem/agent/factories/__init__.py +16 -0
- powermem/agent/factories/agent_factory.py +266 -0
- powermem/agent/factories/config_factory.py +308 -0
- powermem/agent/factories/memory_factory.py +229 -0
- powermem/agent/implementations/__init__.py +16 -0
- powermem/agent/implementations/hybrid.py +728 -0
- powermem/agent/implementations/multi_agent.py +1040 -0
- powermem/agent/implementations/multi_user.py +1020 -0
- powermem/agent/types.py +53 -0
- powermem/agent/wrappers/__init__.py +14 -0
- powermem/agent/wrappers/agent_memory_wrapper.py +427 -0
- powermem/agent/wrappers/compatibility_wrapper.py +520 -0
- powermem/config_loader.py +318 -0
- powermem/configs.py +249 -0
- powermem/core/__init__.py +19 -0
- powermem/core/async_memory.py +1493 -0
- powermem/core/audit.py +258 -0
- powermem/core/base.py +165 -0
- powermem/core/memory.py +1567 -0
- powermem/core/setup.py +162 -0
- powermem/core/telemetry.py +215 -0
- powermem/integrations/__init__.py +17 -0
- powermem/integrations/embeddings/__init__.py +13 -0
- powermem/integrations/embeddings/aws_bedrock.py +100 -0
- powermem/integrations/embeddings/azure_openai.py +55 -0
- powermem/integrations/embeddings/base.py +31 -0
- powermem/integrations/embeddings/config/base.py +132 -0
- powermem/integrations/embeddings/configs.py +31 -0
- powermem/integrations/embeddings/factory.py +48 -0
- powermem/integrations/embeddings/gemini.py +39 -0
- powermem/integrations/embeddings/huggingface.py +41 -0
- powermem/integrations/embeddings/langchain.py +35 -0
- powermem/integrations/embeddings/lmstudio.py +29 -0
- powermem/integrations/embeddings/mock.py +11 -0
- powermem/integrations/embeddings/ollama.py +53 -0
- powermem/integrations/embeddings/openai.py +49 -0
- powermem/integrations/embeddings/qwen.py +102 -0
- powermem/integrations/embeddings/together.py +31 -0
- powermem/integrations/embeddings/vertexai.py +54 -0
- powermem/integrations/llm/__init__.py +18 -0
- powermem/integrations/llm/anthropic.py +87 -0
- powermem/integrations/llm/base.py +132 -0
- powermem/integrations/llm/config/anthropic.py +56 -0
- powermem/integrations/llm/config/azure.py +56 -0
- powermem/integrations/llm/config/base.py +62 -0
- powermem/integrations/llm/config/deepseek.py +56 -0
- powermem/integrations/llm/config/ollama.py +56 -0
- powermem/integrations/llm/config/openai.py +79 -0
- powermem/integrations/llm/config/qwen.py +68 -0
- powermem/integrations/llm/config/qwen_asr.py +46 -0
- powermem/integrations/llm/config/vllm.py +56 -0
- powermem/integrations/llm/configs.py +26 -0
- powermem/integrations/llm/deepseek.py +106 -0
- powermem/integrations/llm/factory.py +118 -0
- powermem/integrations/llm/gemini.py +201 -0
- powermem/integrations/llm/langchain.py +65 -0
- powermem/integrations/llm/ollama.py +106 -0
- powermem/integrations/llm/openai.py +166 -0
- powermem/integrations/llm/openai_structured.py +80 -0
- powermem/integrations/llm/qwen.py +207 -0
- powermem/integrations/llm/qwen_asr.py +171 -0
- powermem/integrations/llm/vllm.py +106 -0
- powermem/integrations/rerank/__init__.py +20 -0
- powermem/integrations/rerank/base.py +43 -0
- powermem/integrations/rerank/config/__init__.py +7 -0
- powermem/integrations/rerank/config/base.py +27 -0
- powermem/integrations/rerank/configs.py +23 -0
- powermem/integrations/rerank/factory.py +68 -0
- powermem/integrations/rerank/qwen.py +159 -0
- powermem/intelligence/__init__.py +17 -0
- powermem/intelligence/ebbinghaus_algorithm.py +354 -0
- powermem/intelligence/importance_evaluator.py +361 -0
- powermem/intelligence/intelligent_memory_manager.py +284 -0
- powermem/intelligence/manager.py +148 -0
- powermem/intelligence/plugin.py +229 -0
- powermem/prompts/__init__.py +29 -0
- powermem/prompts/graph/graph_prompts.py +217 -0
- powermem/prompts/graph/graph_tools_prompts.py +469 -0
- powermem/prompts/importance_evaluation.py +246 -0
- powermem/prompts/intelligent_memory_prompts.py +163 -0
- powermem/prompts/templates.py +193 -0
- powermem/storage/__init__.py +14 -0
- powermem/storage/adapter.py +896 -0
- powermem/storage/base.py +109 -0
- powermem/storage/config/base.py +13 -0
- powermem/storage/config/oceanbase.py +58 -0
- powermem/storage/config/pgvector.py +52 -0
- powermem/storage/config/sqlite.py +27 -0
- powermem/storage/configs.py +159 -0
- powermem/storage/factory.py +59 -0
- powermem/storage/migration_manager.py +438 -0
- powermem/storage/oceanbase/__init__.py +8 -0
- powermem/storage/oceanbase/constants.py +162 -0
- powermem/storage/oceanbase/oceanbase.py +1384 -0
- powermem/storage/oceanbase/oceanbase_graph.py +1441 -0
- powermem/storage/pgvector/__init__.py +7 -0
- powermem/storage/pgvector/pgvector.py +420 -0
- powermem/storage/sqlite/__init__.py +0 -0
- powermem/storage/sqlite/sqlite.py +218 -0
- powermem/storage/sqlite/sqlite_vector_store.py +311 -0
- powermem/utils/__init__.py +35 -0
- powermem/utils/utils.py +605 -0
- powermem/version.py +23 -0
- powermem-0.1.0.dist-info/METADATA +187 -0
- powermem-0.1.0.dist-info/RECORD +123 -0
- powermem-0.1.0.dist-info/WHEEL +5 -0
- powermem-0.1.0.dist-info/licenses/LICENSE +206 -0
- powermem-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
from powermem.integrations.llm import LLMBase
|
|
4
|
+
from powermem.integrations.llm.config.base import BaseLLMConfig
|
|
5
|
+
from powermem.integrations.llm.config.ollama import OllamaConfig
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from ollama import Client
|
|
9
|
+
except ImportError:
|
|
10
|
+
raise ImportError("The 'ollama' library is required. Please install it using 'pip install ollama'.")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OllamaLLM(LLMBase):
|
|
14
|
+
def __init__(self, config: Optional[Union[BaseLLMConfig, OllamaConfig, Dict]] = None):
|
|
15
|
+
# Convert to OllamaConfig if needed
|
|
16
|
+
if config is None:
|
|
17
|
+
config = OllamaConfig()
|
|
18
|
+
elif isinstance(config, dict):
|
|
19
|
+
config = OllamaConfig(**config)
|
|
20
|
+
elif isinstance(config, BaseLLMConfig) and not isinstance(config, OllamaConfig):
|
|
21
|
+
# Convert BaseLLMConfig to OllamaConfig
|
|
22
|
+
config = OllamaConfig(
|
|
23
|
+
model=config.model,
|
|
24
|
+
temperature=config.temperature,
|
|
25
|
+
api_key=config.api_key,
|
|
26
|
+
max_tokens=config.max_tokens,
|
|
27
|
+
top_p=config.top_p,
|
|
28
|
+
top_k=config.top_k,
|
|
29
|
+
enable_vision=config.enable_vision,
|
|
30
|
+
vision_details=config.vision_details,
|
|
31
|
+
http_client_proxies=config.http_client,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
super().__init__(config)
|
|
35
|
+
|
|
36
|
+
if not self.config.model:
|
|
37
|
+
self.config.model = "llama3.1:70b"
|
|
38
|
+
|
|
39
|
+
self.client = Client(host=self.config.ollama_base_url)
|
|
40
|
+
|
|
41
|
+
def _parse_response(self, response, tools):
|
|
42
|
+
"""
|
|
43
|
+
Process the response based on whether tools are used or not.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
response: The raw response from API.
|
|
47
|
+
tools: The list of tools provided in the request.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str or dict: The processed response.
|
|
51
|
+
"""
|
|
52
|
+
if tools:
|
|
53
|
+
processed_response = {
|
|
54
|
+
"content": response["message"]["content"] if isinstance(response, dict) else response.message.content,
|
|
55
|
+
"tool_calls": [],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Ollama doesn't support tool calls in the same way, so we return the content
|
|
59
|
+
return processed_response
|
|
60
|
+
else:
|
|
61
|
+
# Handle both dict and object responses
|
|
62
|
+
if isinstance(response, dict):
|
|
63
|
+
return response["message"]["content"]
|
|
64
|
+
else:
|
|
65
|
+
return response.message.content
|
|
66
|
+
|
|
67
|
+
def generate_response(
|
|
68
|
+
self,
|
|
69
|
+
messages: List[Dict[str, str]],
|
|
70
|
+
response_format=None,
|
|
71
|
+
tools: Optional[List[Dict]] = None,
|
|
72
|
+
tool_choice: str = "auto",
|
|
73
|
+
**kwargs,
|
|
74
|
+
):
|
|
75
|
+
"""
|
|
76
|
+
Generate a response based on the given messages using Ollama.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
messages (list): List of message dicts containing 'role' and 'content'.
|
|
80
|
+
response_format (str or object, optional): Format of the response. Defaults to "text".
|
|
81
|
+
tools (list, optional): List of tools that the model can call. Defaults to None.
|
|
82
|
+
tool_choice (str, optional): Tool choice method. Defaults to "auto".
|
|
83
|
+
**kwargs: Additional Ollama-specific parameters.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
str: The generated response.
|
|
87
|
+
"""
|
|
88
|
+
# Build parameters for Ollama
|
|
89
|
+
params = {
|
|
90
|
+
"model": self.config.model,
|
|
91
|
+
"messages": messages,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Add options for Ollama (temperature, num_predict, top_p)
|
|
95
|
+
options = {
|
|
96
|
+
"temperature": self.config.temperature,
|
|
97
|
+
"num_predict": self.config.max_tokens,
|
|
98
|
+
"top_p": self.config.top_p,
|
|
99
|
+
}
|
|
100
|
+
params["options"] = options
|
|
101
|
+
|
|
102
|
+
# Remove OpenAI-specific parameters that Ollama doesn't support
|
|
103
|
+
params.pop("max_tokens", None) # Ollama uses different parameter names
|
|
104
|
+
|
|
105
|
+
response = self.client.chat(**params)
|
|
106
|
+
return self._parse_response(response, tools)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
from openai import OpenAI
|
|
7
|
+
from powermem.integrations.llm import LLMBase
|
|
8
|
+
from powermem.integrations.llm.config.base import BaseLLMConfig
|
|
9
|
+
from powermem.integrations.llm.config.openai import OpenAIConfig
|
|
10
|
+
from powermem.utils.utils import extract_json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenAILLM(LLMBase):
|
|
14
|
+
def __init__(self, config: Optional[Union[BaseLLMConfig, OpenAIConfig, Dict]] = None):
|
|
15
|
+
# Convert to OpenAIConfig if needed
|
|
16
|
+
if config is None:
|
|
17
|
+
config = OpenAIConfig()
|
|
18
|
+
elif isinstance(config, dict):
|
|
19
|
+
config = OpenAIConfig(**config)
|
|
20
|
+
elif isinstance(config, BaseLLMConfig) and not isinstance(config, OpenAIConfig):
|
|
21
|
+
# Convert BaseLLMConfig to OpenAIConfig
|
|
22
|
+
config = OpenAIConfig(
|
|
23
|
+
model=config.model,
|
|
24
|
+
temperature=config.temperature,
|
|
25
|
+
api_key=config.api_key,
|
|
26
|
+
max_tokens=config.max_tokens,
|
|
27
|
+
top_p=config.top_p,
|
|
28
|
+
top_k=config.top_k,
|
|
29
|
+
enable_vision=config.enable_vision,
|
|
30
|
+
vision_details=config.vision_details,
|
|
31
|
+
http_client_proxies=config.http_client,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
super().__init__(config)
|
|
35
|
+
|
|
36
|
+
if not self.config.model:
|
|
37
|
+
self.config.model = "gpt-4o-mini"
|
|
38
|
+
|
|
39
|
+
if os.environ.get("OPENROUTER_API_KEY"): # Use OpenRouter
|
|
40
|
+
self.client = OpenAI(
|
|
41
|
+
api_key=os.environ.get("OPENROUTER_API_KEY"),
|
|
42
|
+
base_url=self.config.openrouter_base_url
|
|
43
|
+
or os.getenv("OPENROUTER_API_BASE")
|
|
44
|
+
or "https://openrouter.ai/api/v1",
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
api_key = self.config.api_key or os.getenv("OPENAI_API_KEY")
|
|
48
|
+
base_url = self.config.openai_base_url or os.getenv("OPENAI_BASE_URL") or "https://api.openai.com/v1"
|
|
49
|
+
|
|
50
|
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
|
51
|
+
|
|
52
|
+
def _parse_response(self, response, tools):
|
|
53
|
+
"""
|
|
54
|
+
Process the response based on whether tools are used or not.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
response: The raw response from API.
|
|
58
|
+
tools: The list of tools provided in the request.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
str or dict: The processed response.
|
|
62
|
+
"""
|
|
63
|
+
if tools:
|
|
64
|
+
processed_response = {
|
|
65
|
+
"content": response.choices[0].message.content,
|
|
66
|
+
"tool_calls": [],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if response.choices[0].message.tool_calls:
|
|
70
|
+
for tool_call in response.choices[0].message.tool_calls:
|
|
71
|
+
# Extract and validate arguments
|
|
72
|
+
arguments_str = extract_json(tool_call.function.arguments)
|
|
73
|
+
|
|
74
|
+
# Check if arguments are empty or whitespace only
|
|
75
|
+
if not arguments_str or arguments_str.strip() == "":
|
|
76
|
+
logging.warning(
|
|
77
|
+
f"Tool call '{tool_call.function.name}' has empty arguments. Skipping this tool call."
|
|
78
|
+
)
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Try to parse JSON with error handling
|
|
82
|
+
try:
|
|
83
|
+
arguments = json.loads(arguments_str)
|
|
84
|
+
except json.JSONDecodeError as e:
|
|
85
|
+
logging.error(
|
|
86
|
+
f"Failed to parse tool call arguments for '{tool_call.function.name}': "
|
|
87
|
+
f"{arguments_str[:100]}... Error: {e}"
|
|
88
|
+
)
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
processed_response["tool_calls"].append(
|
|
92
|
+
{
|
|
93
|
+
"name": tool_call.function.name,
|
|
94
|
+
"arguments": arguments,
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return processed_response
|
|
99
|
+
else:
|
|
100
|
+
return response.choices[0].message.content
|
|
101
|
+
|
|
102
|
+
def generate_response(
|
|
103
|
+
self,
|
|
104
|
+
messages: List[Dict[str, str]],
|
|
105
|
+
response_format=None,
|
|
106
|
+
tools: Optional[List[Dict]] = None,
|
|
107
|
+
tool_choice: str = "auto",
|
|
108
|
+
**kwargs,
|
|
109
|
+
):
|
|
110
|
+
"""
|
|
111
|
+
Generate a JSON response based on the given messages using OpenAI.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
messages (list): List of message dicts containing 'role' and 'content'.
|
|
115
|
+
response_format (str or object, optional): Format of the response. Defaults to "text".
|
|
116
|
+
tools (list, optional): List of tools that the model can call. Defaults to None.
|
|
117
|
+
tool_choice (str, optional): Tool choice method. Defaults to "auto".
|
|
118
|
+
**kwargs: Additional OpenAI-specific parameters.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
json: The generated response.
|
|
122
|
+
"""
|
|
123
|
+
params = self._get_supported_params(messages=messages, **kwargs)
|
|
124
|
+
|
|
125
|
+
params.update({
|
|
126
|
+
"model": self.config.model,
|
|
127
|
+
"messages": messages,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
if os.getenv("OPENROUTER_API_KEY"):
|
|
131
|
+
openrouter_params = {}
|
|
132
|
+
if self.config.models:
|
|
133
|
+
openrouter_params["models"] = self.config.models
|
|
134
|
+
openrouter_params["route"] = self.config.route
|
|
135
|
+
params.pop("model")
|
|
136
|
+
|
|
137
|
+
if self.config.site_url and self.config.app_name:
|
|
138
|
+
extra_headers = {
|
|
139
|
+
"HTTP-Referer": self.config.site_url,
|
|
140
|
+
"X-Title": self.config.app_name,
|
|
141
|
+
}
|
|
142
|
+
openrouter_params["extra_headers"] = extra_headers
|
|
143
|
+
|
|
144
|
+
params.update(**openrouter_params)
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
openai_specific_generation_params = ["store"]
|
|
148
|
+
for param in openai_specific_generation_params:
|
|
149
|
+
if hasattr(self.config, param):
|
|
150
|
+
params[param] = getattr(self.config, param)
|
|
151
|
+
|
|
152
|
+
if response_format:
|
|
153
|
+
params["response_format"] = response_format
|
|
154
|
+
if tools: # TODO: Remove tools if no issues found with new memory addition logic
|
|
155
|
+
params["tools"] = tools
|
|
156
|
+
params["tool_choice"] = tool_choice
|
|
157
|
+
response = self.client.chat.completions.create(**params)
|
|
158
|
+
parsed_response = self._parse_response(response, tools)
|
|
159
|
+
if self.config.response_callback:
|
|
160
|
+
try:
|
|
161
|
+
self.config.response_callback(self, response, params)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
# Log error but don't propagate
|
|
164
|
+
logging.error(f"Error due to callback: {e}")
|
|
165
|
+
pass
|
|
166
|
+
return parsed_response
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Dict, List, Optional, Any
|
|
4
|
+
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
|
|
7
|
+
from powermem.integrations.llm import LLMBase
|
|
8
|
+
from powermem.integrations.llm.config.base import BaseLLMConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OpenAIStructuredLLM(LLMBase):
|
|
12
|
+
def __init__(self, config: Optional[BaseLLMConfig] = None):
|
|
13
|
+
super().__init__(config)
|
|
14
|
+
|
|
15
|
+
if not self.config.model:
|
|
16
|
+
self.config.model = "gpt-5"
|
|
17
|
+
|
|
18
|
+
api_key = self.config.api_key or os.getenv("OPENAI_API_KEY")
|
|
19
|
+
base_url = self.config.openai_base_url or os.getenv("OPENAI_API_BASE") or "https://api.openai.com/v1"
|
|
20
|
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
|
21
|
+
|
|
22
|
+
def generate_response(
|
|
23
|
+
self,
|
|
24
|
+
messages: List[Dict[str, str]],
|
|
25
|
+
response_format: Optional[str] = None,
|
|
26
|
+
tools: Optional[List[Dict]] = None,
|
|
27
|
+
tool_choice: str = "auto",
|
|
28
|
+
) -> str | None | dict[str, str | None | list[Any]]:
|
|
29
|
+
"""
|
|
30
|
+
Generate a response based on the given messages using OpenAI.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
messages (List[Dict[str, str]]): A list of dictionaries, each containing a 'role' and 'content' key.
|
|
34
|
+
response_format (Optional[str]): The desired format of the response. Defaults to None.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
str: The generated response.
|
|
39
|
+
"""
|
|
40
|
+
params = {
|
|
41
|
+
"model": self.config.model,
|
|
42
|
+
"messages": messages,
|
|
43
|
+
"temperature": self.config.temperature,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if response_format:
|
|
47
|
+
params["response_format"] = response_format
|
|
48
|
+
if tools:
|
|
49
|
+
params["tools"] = tools
|
|
50
|
+
params["tool_choice"] = tool_choice
|
|
51
|
+
|
|
52
|
+
response = self.client.beta.chat.completions.parse(**params)
|
|
53
|
+
|
|
54
|
+
message = response.choices[0].message
|
|
55
|
+
|
|
56
|
+
# If tools were used and tool_calls exist, return structured response
|
|
57
|
+
if tools and message.tool_calls:
|
|
58
|
+
tool_calls_list = []
|
|
59
|
+
for tool_call in message.tool_calls:
|
|
60
|
+
arguments = tool_call.function.arguments
|
|
61
|
+
# Parse arguments if it's a string
|
|
62
|
+
if isinstance(arguments, str):
|
|
63
|
+
try:
|
|
64
|
+
arguments = json.loads(arguments)
|
|
65
|
+
except json.JSONDecodeError:
|
|
66
|
+
arguments = {}
|
|
67
|
+
|
|
68
|
+
tool_calls_list.append({
|
|
69
|
+
"id": tool_call.id,
|
|
70
|
+
"name": tool_call.function.name,
|
|
71
|
+
"arguments": arguments
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
"content": message.content,
|
|
76
|
+
"tool_calls": tool_calls_list
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Otherwise return just the content
|
|
80
|
+
return message.content or ""
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from typing import Dict, List, Optional, Union
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from dashscope import Generation
|
|
8
|
+
from dashscope.api_entities.dashscope_response import DashScopeAPIResponse
|
|
9
|
+
except ImportError:
|
|
10
|
+
Generation = None
|
|
11
|
+
DashScopeAPIResponse = None
|
|
12
|
+
|
|
13
|
+
from powermem.integrations.llm import LLMBase
|
|
14
|
+
from powermem.integrations.llm.config.base import BaseLLMConfig
|
|
15
|
+
from powermem.integrations.llm.config.qwen import QwenConfig
|
|
16
|
+
from powermem.utils.utils import extract_json
|
|
17
|
+
import dashscope
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class QwenLLM(LLMBase):
|
|
21
|
+
def __init__(self, config: Optional[Union[BaseLLMConfig, QwenConfig, Dict]] = None):
|
|
22
|
+
# Check if dashscope is available first
|
|
23
|
+
try:
|
|
24
|
+
from dashscope import Generation
|
|
25
|
+
from dashscope.api_entities.dashscope_response import DashScopeAPIResponse
|
|
26
|
+
except ImportError:
|
|
27
|
+
raise ImportError(
|
|
28
|
+
"DashScope SDK is not installed. Please install it with: pip install dashscope"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Convert to QwenConfig if needed
|
|
32
|
+
if config is None:
|
|
33
|
+
config = QwenConfig()
|
|
34
|
+
elif isinstance(config, dict):
|
|
35
|
+
config = QwenConfig(**config)
|
|
36
|
+
elif isinstance(config, BaseLLMConfig) and not isinstance(config, QwenConfig):
|
|
37
|
+
# Convert BaseLLMConfig to QwenConfig
|
|
38
|
+
config = QwenConfig(
|
|
39
|
+
model=config.model,
|
|
40
|
+
temperature=config.temperature,
|
|
41
|
+
api_key=config.api_key,
|
|
42
|
+
max_tokens=config.max_tokens,
|
|
43
|
+
top_p=config.top_p,
|
|
44
|
+
top_k=config.top_k,
|
|
45
|
+
enable_vision=config.enable_vision,
|
|
46
|
+
vision_details=config.vision_details,
|
|
47
|
+
http_client_proxies=config.http_client,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
super().__init__(config)
|
|
51
|
+
|
|
52
|
+
if not self.config.model:
|
|
53
|
+
self.config.model = "qwen-turbo"
|
|
54
|
+
|
|
55
|
+
# Set API key
|
|
56
|
+
api_key = self.config.api_key or os.getenv("DASHSCOPE_API_KEY")
|
|
57
|
+
if not api_key:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
"API key is required. Set DASHSCOPE_API_KEY environment variable or pass api_key in config.")
|
|
60
|
+
|
|
61
|
+
# Set API key for DashScope SDK
|
|
62
|
+
dashscope.api_key = api_key
|
|
63
|
+
|
|
64
|
+
# Set base URL
|
|
65
|
+
base_url = self.config.dashscope_base_url or os.getenv(
|
|
66
|
+
"DASHSCOPE_BASE_URL") or "https://dashscope.aliyuncs.com/api/v1"
|
|
67
|
+
|
|
68
|
+
if base_url:
|
|
69
|
+
os.environ["DASHSCOPE_BASE_URL"] = base_url
|
|
70
|
+
|
|
71
|
+
def _get_attr(self, obj, key, default=None):
|
|
72
|
+
"""Unified handling of attribute access for both dicts and objects"""
|
|
73
|
+
if isinstance(obj, dict):
|
|
74
|
+
return obj.get(key, default)
|
|
75
|
+
return getattr(obj, key, default)
|
|
76
|
+
|
|
77
|
+
def _extract_message(self, output):
|
|
78
|
+
"""Extract message object from response output"""
|
|
79
|
+
# Get default text content
|
|
80
|
+
text = self._get_attr(output, 'text', '')
|
|
81
|
+
|
|
82
|
+
# Extract message from choices
|
|
83
|
+
choices = self._get_attr(output, 'choices', [])
|
|
84
|
+
if choices:
|
|
85
|
+
choice = choices[0]
|
|
86
|
+
message = self._get_attr(choice, 'message')
|
|
87
|
+
return message, text
|
|
88
|
+
|
|
89
|
+
return None, text
|
|
90
|
+
|
|
91
|
+
def _extract_content(self, output):
|
|
92
|
+
"""Extract response content"""
|
|
93
|
+
message, default_text = self._extract_message(output)
|
|
94
|
+
if message:
|
|
95
|
+
return self._get_attr(message, 'content', default_text)
|
|
96
|
+
return default_text
|
|
97
|
+
|
|
98
|
+
def _extract_tool_calls(self, output):
|
|
99
|
+
"""Extract tool calls from response"""
|
|
100
|
+
message, _ = self._extract_message(output)
|
|
101
|
+
if not message:
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
tool_calls = self._get_attr(message, 'tool_calls')
|
|
105
|
+
if not tool_calls:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
processed_calls = []
|
|
109
|
+
for tool_call in tool_calls:
|
|
110
|
+
function = self._get_attr(tool_call, 'function', {})
|
|
111
|
+
name = self._get_attr(function, 'name')
|
|
112
|
+
arguments = self._get_attr(function, 'arguments', '{}')
|
|
113
|
+
|
|
114
|
+
processed_calls.append({
|
|
115
|
+
"name": name,
|
|
116
|
+
"arguments": json.loads(extract_json(arguments)),
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return processed_calls
|
|
120
|
+
|
|
121
|
+
def _parse_response(self, response: DashScopeAPIResponse, tools: Optional[List[Dict]] = None):
|
|
122
|
+
"""
|
|
123
|
+
Process the response based on whether tools are used or not.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
response: The raw response from DashScope API.
|
|
127
|
+
tools: The list of tools provided in the request.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
str or dict: The processed response.
|
|
131
|
+
"""
|
|
132
|
+
if response.status_code != 200:
|
|
133
|
+
raise Exception(f"API request failed with status {response.status_code}: {response.message}")
|
|
134
|
+
|
|
135
|
+
content = self._extract_content(response.output)
|
|
136
|
+
|
|
137
|
+
if tools:
|
|
138
|
+
return {
|
|
139
|
+
"content": content,
|
|
140
|
+
"tool_calls": self._extract_tool_calls(response.output),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return content
|
|
144
|
+
|
|
145
|
+
def generate_response(
|
|
146
|
+
self,
|
|
147
|
+
messages: List[Dict[str, str]],
|
|
148
|
+
response_format=None,
|
|
149
|
+
tools: Optional[List[Dict]] = None,
|
|
150
|
+
tool_choice: str = "auto",
|
|
151
|
+
**kwargs,
|
|
152
|
+
):
|
|
153
|
+
"""
|
|
154
|
+
Generate a response based on the given messages using Qwen.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
messages (list): List of message dicts containing 'role' and 'content'.
|
|
158
|
+
response_format (str or object, optional): Format of the response. Defaults to None.
|
|
159
|
+
tools (list, optional): List of tools that the model can call. Defaults to None.
|
|
160
|
+
tool_choice (str, optional): Tool choice method. Defaults to "auto".
|
|
161
|
+
**kwargs: Additional Qwen-specific parameters.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
str or dict: The generated response.
|
|
165
|
+
"""
|
|
166
|
+
params = self._get_supported_params(**kwargs)
|
|
167
|
+
|
|
168
|
+
# Prepare generation parameters
|
|
169
|
+
generation_params = {
|
|
170
|
+
"model": self.config.model,
|
|
171
|
+
"messages": messages,
|
|
172
|
+
"temperature": params.get("temperature", self.config.temperature),
|
|
173
|
+
"max_tokens": params.get("max_tokens", self.config.max_tokens),
|
|
174
|
+
"top_p": params.get("top_p", self.config.top_p),
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Add Qwen-specific parameters
|
|
178
|
+
if self.config.enable_search:
|
|
179
|
+
generation_params["enable_search"] = True
|
|
180
|
+
if self.config.search_params:
|
|
181
|
+
generation_params.update(self.config.search_params)
|
|
182
|
+
|
|
183
|
+
# Add tools if provided
|
|
184
|
+
if tools:
|
|
185
|
+
generation_params["tools"] = tools
|
|
186
|
+
generation_params["tool_choice"] = tool_choice
|
|
187
|
+
|
|
188
|
+
# Add response format if provided
|
|
189
|
+
if response_format:
|
|
190
|
+
generation_params["response_format"] = response_format
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
response = Generation.call(**generation_params)
|
|
194
|
+
parsed_response = self._parse_response(response, tools)
|
|
195
|
+
|
|
196
|
+
if self.config.response_callback:
|
|
197
|
+
try:
|
|
198
|
+
self.config.response_callback(self, response, generation_params)
|
|
199
|
+
except Exception as e:
|
|
200
|
+
# Log error but don't propagate
|
|
201
|
+
logging.error(f"Error due to callback: {e}")
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
return parsed_response
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logging.error(f"Qwen API call failed: {e}")
|
|
207
|
+
raise
|