autobyteus 1.1.0__py3-none-any.whl → 1.1.2__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.
- autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +1 -1
- autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +1 -1
- autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +1 -1
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +1 -1
- autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +1 -1
- autobyteus/agent/context/__init__.py +0 -5
- autobyteus/agent/context/agent_config.py +6 -2
- autobyteus/agent/context/agent_context.py +2 -5
- autobyteus/agent/context/agent_phase_manager.py +105 -5
- autobyteus/agent/context/agent_runtime_state.py +2 -2
- autobyteus/agent/context/phases.py +2 -0
- autobyteus/agent/events/__init__.py +0 -11
- autobyteus/agent/events/agent_events.py +0 -37
- autobyteus/agent/events/notifiers.py +25 -7
- autobyteus/agent/events/worker_event_dispatcher.py +1 -1
- autobyteus/agent/factory/agent_factory.py +6 -2
- autobyteus/agent/group/agent_group.py +16 -7
- autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +28 -14
- autobyteus/agent/handlers/lifecycle_event_logger.py +1 -1
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +4 -2
- autobyteus/agent/handlers/tool_invocation_request_event_handler.py +40 -15
- autobyteus/agent/handlers/tool_result_event_handler.py +12 -7
- autobyteus/agent/hooks/__init__.py +7 -0
- autobyteus/agent/hooks/base_phase_hook.py +11 -2
- autobyteus/agent/hooks/hook_definition.py +36 -0
- autobyteus/agent/hooks/hook_meta.py +37 -0
- autobyteus/agent/hooks/hook_registry.py +118 -0
- autobyteus/agent/input_processor/base_user_input_processor.py +6 -3
- autobyteus/agent/input_processor/passthrough_input_processor.py +2 -1
- autobyteus/agent/input_processor/processor_meta.py +1 -1
- autobyteus/agent/input_processor/processor_registry.py +19 -0
- autobyteus/agent/llm_response_processor/base_processor.py +6 -3
- autobyteus/agent/llm_response_processor/processor_meta.py +1 -1
- autobyteus/agent/llm_response_processor/processor_registry.py +19 -0
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +2 -1
- autobyteus/agent/message/context_file_type.py +2 -3
- autobyteus/agent/phases/__init__.py +18 -0
- autobyteus/agent/phases/discover.py +52 -0
- autobyteus/agent/phases/manager.py +265 -0
- autobyteus/agent/phases/phase_enum.py +49 -0
- autobyteus/agent/phases/transition_decorator.py +40 -0
- autobyteus/agent/phases/transition_info.py +33 -0
- autobyteus/agent/remote_agent.py +1 -1
- autobyteus/agent/runtime/agent_runtime.py +5 -10
- autobyteus/agent/runtime/agent_worker.py +62 -19
- autobyteus/agent/streaming/agent_event_stream.py +58 -5
- autobyteus/agent/streaming/stream_event_payloads.py +24 -13
- autobyteus/agent/streaming/stream_events.py +14 -11
- autobyteus/agent/system_prompt_processor/base_processor.py +6 -3
- autobyteus/agent/system_prompt_processor/processor_meta.py +1 -1
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +45 -31
- autobyteus/agent/tool_invocation.py +29 -3
- autobyteus/agent/utils/wait_for_idle.py +1 -1
- autobyteus/agent/workspace/__init__.py +2 -0
- autobyteus/agent/workspace/base_workspace.py +33 -11
- autobyteus/agent/workspace/workspace_config.py +160 -0
- autobyteus/agent/workspace/workspace_definition.py +36 -0
- autobyteus/agent/workspace/workspace_meta.py +37 -0
- autobyteus/agent/workspace/workspace_registry.py +72 -0
- autobyteus/cli/__init__.py +4 -3
- autobyteus/cli/agent_cli.py +25 -207
- autobyteus/cli/cli_display.py +205 -0
- autobyteus/events/event_manager.py +2 -1
- autobyteus/events/event_types.py +3 -1
- autobyteus/llm/api/autobyteus_llm.py +2 -12
- autobyteus/llm/api/deepseek_llm.py +11 -173
- autobyteus/llm/api/grok_llm.py +11 -172
- autobyteus/llm/api/kimi_llm.py +24 -0
- autobyteus/llm/api/mistral_llm.py +4 -4
- autobyteus/llm/api/ollama_llm.py +2 -2
- autobyteus/llm/api/openai_compatible_llm.py +193 -0
- autobyteus/llm/api/openai_llm.py +11 -139
- autobyteus/llm/extensions/token_usage_tracking_extension.py +11 -1
- autobyteus/llm/llm_factory.py +168 -42
- autobyteus/llm/models.py +25 -29
- autobyteus/llm/ollama_provider.py +6 -2
- autobyteus/llm/ollama_provider_resolver.py +44 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/kimi_token_counter.py +24 -0
- autobyteus/llm/token_counter/token_counter_factory.py +3 -0
- autobyteus/llm/utils/messages.py +3 -3
- autobyteus/tools/__init__.py +2 -0
- autobyteus/tools/base_tool.py +7 -1
- autobyteus/tools/functional_tool.py +20 -5
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +15 -1
- autobyteus/tools/mcp/config_service.py +106 -127
- autobyteus/tools/mcp/registrar.py +247 -59
- autobyteus/tools/mcp/types.py +5 -3
- autobyteus/tools/registry/tool_definition.py +8 -1
- autobyteus/tools/registry/tool_registry.py +18 -0
- autobyteus/tools/tool_category.py +11 -0
- autobyteus/tools/tool_meta.py +3 -1
- autobyteus/tools/tool_state.py +20 -0
- autobyteus/tools/usage/parsers/_json_extractor.py +99 -0
- autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +46 -77
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +87 -96
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +37 -47
- autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +112 -113
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/METADATA +13 -12
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/RECORD +103 -82
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,188 +1,26 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import
|
|
3
|
-
from typing import Optional, List, AsyncGenerator
|
|
4
|
-
from openai import OpenAI
|
|
5
|
-
from openai.types.completion_usage import CompletionUsage
|
|
6
|
-
from openai.types.chat import ChatCompletionChunk
|
|
7
|
-
from autobyteus.llm.base_llm import BaseLLM
|
|
2
|
+
from typing import Optional
|
|
8
3
|
from autobyteus.llm.models import LLMModel
|
|
9
4
|
from autobyteus.llm.utils.llm_config import LLMConfig
|
|
10
|
-
from autobyteus.llm.
|
|
11
|
-
from autobyteus.llm.utils.image_payload_formatter import process_image
|
|
12
|
-
from autobyteus.llm.utils.token_usage import TokenUsage
|
|
13
|
-
from autobyteus.llm.utils.response_types import CompleteResponse, ChunkResponse
|
|
5
|
+
from autobyteus.llm.api.openai_compatible_llm import OpenAICompatibleLLM
|
|
14
6
|
|
|
15
7
|
logger = logging.getLogger(__name__)
|
|
16
8
|
|
|
17
|
-
class DeepSeekLLM(
|
|
9
|
+
class DeepSeekLLM(OpenAICompatibleLLM):
|
|
18
10
|
def __init__(self, model: LLMModel = None, llm_config: LLMConfig = None):
|
|
19
|
-
deepseek_api_key = os.getenv("DEEPSEEK_API_KEY")
|
|
20
|
-
if not deepseek_api_key:
|
|
21
|
-
logger.error("DEEPSEEK_API_KEY environment variable is not set.")
|
|
22
|
-
raise ValueError("DEEPSEEK_API_KEY environment variable is not set.")
|
|
23
|
-
|
|
24
|
-
self.client = OpenAI(api_key=deepseek_api_key, base_url="https://api.deepseek.com")
|
|
25
|
-
logger.info("DeepSeek API key and base URL set successfully")
|
|
26
|
-
|
|
27
11
|
# Provide defaults if not specified
|
|
28
12
|
if model is None:
|
|
29
|
-
model = LLMModel
|
|
13
|
+
model = LLMModel['deepseek-chat']
|
|
30
14
|
if llm_config is None:
|
|
31
15
|
llm_config = LLMConfig()
|
|
32
16
|
|
|
33
|
-
super().__init__(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if not usage_data:
|
|
39
|
-
return None
|
|
40
|
-
|
|
41
|
-
return TokenUsage(
|
|
42
|
-
prompt_tokens=usage_data.prompt_tokens,
|
|
43
|
-
completion_tokens=usage_data.completion_tokens,
|
|
44
|
-
total_tokens=usage_data.total_tokens
|
|
17
|
+
super().__init__(
|
|
18
|
+
model=model,
|
|
19
|
+
llm_config=llm_config,
|
|
20
|
+
api_key_env_var="DEEPSEEK_API_KEY",
|
|
21
|
+
base_url="https://api.deepseek.com"
|
|
45
22
|
)
|
|
23
|
+
logger.info(f"DeepSeekLLM initialized with model: {self.model}")
|
|
46
24
|
|
|
47
|
-
async def _send_user_message_to_llm(
|
|
48
|
-
self, user_message: str, image_urls: Optional[List[str]] = None, **kwargs
|
|
49
|
-
) -> CompleteResponse:
|
|
50
|
-
"""
|
|
51
|
-
Sends a non-streaming request to the DeepSeek API.
|
|
52
|
-
Supports optional reasoning content if provided in the response.
|
|
53
|
-
"""
|
|
54
|
-
content = []
|
|
55
|
-
|
|
56
|
-
if user_message:
|
|
57
|
-
content.append({"type": "text", "text": user_message})
|
|
58
|
-
|
|
59
|
-
if image_urls:
|
|
60
|
-
for image_url in image_urls:
|
|
61
|
-
try:
|
|
62
|
-
image_content = process_image(image_url)
|
|
63
|
-
content.append(image_content)
|
|
64
|
-
logger.info(f"Processed image: {image_url}")
|
|
65
|
-
except ValueError as e:
|
|
66
|
-
logger.error(f"Error processing image {image_url}: {str(e)}")
|
|
67
|
-
continue
|
|
68
|
-
|
|
69
|
-
self.add_user_message(content)
|
|
70
|
-
logger.debug(f"Prepared message content: {content}")
|
|
71
|
-
|
|
72
|
-
try:
|
|
73
|
-
logger.info("Sending request to DeepSeek API")
|
|
74
|
-
response = self.client.chat.completions.create(
|
|
75
|
-
model=self.model.value,
|
|
76
|
-
messages=[msg.to_dict() for msg in self.messages],
|
|
77
|
-
max_tokens=self.max_tokens,
|
|
78
|
-
)
|
|
79
|
-
full_message = response.choices[0].message
|
|
80
|
-
|
|
81
|
-
# Extract reasoning_content if present
|
|
82
|
-
reasoning = None
|
|
83
|
-
if hasattr(full_message, "reasoning_content") and full_message.reasoning_content:
|
|
84
|
-
reasoning = full_message.reasoning_content
|
|
85
|
-
elif "reasoning_content" in full_message and full_message["reasoning_content"]:
|
|
86
|
-
reasoning = full_message["reasoning_content"]
|
|
87
|
-
|
|
88
|
-
# Extract main content
|
|
89
|
-
main_content = ""
|
|
90
|
-
if hasattr(full_message, "content") and full_message.content:
|
|
91
|
-
main_content = full_message.content
|
|
92
|
-
elif "content" in full_message and full_message["content"]:
|
|
93
|
-
main_content = full_message["content"]
|
|
94
|
-
|
|
95
|
-
self.add_assistant_message(main_content, reasoning_content=reasoning)
|
|
96
|
-
|
|
97
|
-
token_usage = self._create_token_usage(response.usage)
|
|
98
|
-
logger.info("Received response from DeepSeek API with usage data")
|
|
99
|
-
|
|
100
|
-
return CompleteResponse(
|
|
101
|
-
content=main_content,
|
|
102
|
-
reasoning=reasoning,
|
|
103
|
-
usage=token_usage
|
|
104
|
-
)
|
|
105
|
-
except Exception as e:
|
|
106
|
-
logger.error(f"Error in DeepSeek API request: {str(e)}")
|
|
107
|
-
raise ValueError(f"Error in DeepSeek API request: {str(e)}")
|
|
108
|
-
|
|
109
|
-
async def _stream_user_message_to_llm(
|
|
110
|
-
self, user_message: str, image_urls: Optional[List[str]] = None, **kwargs
|
|
111
|
-
) -> AsyncGenerator[ChunkResponse, None]:
|
|
112
|
-
"""
|
|
113
|
-
Streams the response from the DeepSeek API.
|
|
114
|
-
Yields reasoning and content in separate chunks.
|
|
115
|
-
"""
|
|
116
|
-
content = []
|
|
117
|
-
|
|
118
|
-
if user_message:
|
|
119
|
-
content.append({"type": "text", "text": user_message})
|
|
120
|
-
|
|
121
|
-
if image_urls:
|
|
122
|
-
for image_url in image_urls:
|
|
123
|
-
try:
|
|
124
|
-
image_content = process_image(image_url)
|
|
125
|
-
content.append(image_content)
|
|
126
|
-
logger.info(f"Processed image for streaming: {image_url}")
|
|
127
|
-
except ValueError as e:
|
|
128
|
-
logger.error(f"Error processing image for streaming {image_url}: {str(e)}")
|
|
129
|
-
continue
|
|
130
|
-
|
|
131
|
-
self.add_user_message(content)
|
|
132
|
-
logger.debug(f"Prepared streaming message content: {content}")
|
|
133
|
-
|
|
134
|
-
# Initialize variables to track reasoning and main content
|
|
135
|
-
accumulated_reasoning = ""
|
|
136
|
-
accumulated_content = ""
|
|
137
|
-
|
|
138
|
-
try:
|
|
139
|
-
logger.info("Starting streaming request to DeepSeek API")
|
|
140
|
-
stream = self.client.chat.completions.create(
|
|
141
|
-
model=self.model.value,
|
|
142
|
-
messages=[msg.to_dict() for msg in self.messages],
|
|
143
|
-
max_tokens=self.max_tokens,
|
|
144
|
-
stream=True,
|
|
145
|
-
stream_options={"include_usage": True}
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
for chunk in stream:
|
|
149
|
-
chunk: ChatCompletionChunk
|
|
150
|
-
|
|
151
|
-
# Process reasoning tokens
|
|
152
|
-
reasoning_chunk = getattr(chunk.choices[0].delta, "reasoning_content", None)
|
|
153
|
-
if reasoning_chunk:
|
|
154
|
-
accumulated_reasoning += reasoning_chunk
|
|
155
|
-
yield ChunkResponse(
|
|
156
|
-
content="",
|
|
157
|
-
reasoning=reasoning_chunk
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
# Process main content tokens
|
|
161
|
-
main_token = chunk.choices[0].delta.content
|
|
162
|
-
if main_token:
|
|
163
|
-
accumulated_content += main_token
|
|
164
|
-
yield ChunkResponse(
|
|
165
|
-
content=main_token,
|
|
166
|
-
reasoning=None
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
# Yield token usage if available in the final chunk
|
|
170
|
-
if hasattr(chunk, "usage") and chunk.usage is not None:
|
|
171
|
-
token_usage = self._create_token_usage(chunk.usage)
|
|
172
|
-
yield ChunkResponse(
|
|
173
|
-
content="",
|
|
174
|
-
reasoning=None,
|
|
175
|
-
is_complete=True,
|
|
176
|
-
usage=token_usage
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
# After streaming, add the fully accumulated assistant message to history
|
|
180
|
-
self.add_assistant_message(accumulated_content, reasoning_content=accumulated_reasoning)
|
|
181
|
-
logger.info("Completed streaming response from DeepSeek API")
|
|
182
|
-
|
|
183
|
-
except Exception as e:
|
|
184
|
-
logger.error(f"Error in DeepSeek API streaming: {str(e)}")
|
|
185
|
-
raise ValueError(f"Error in DeepSeek API streaming: {str(e)}")
|
|
186
|
-
|
|
187
25
|
async def cleanup(self):
|
|
188
|
-
await super().cleanup()
|
|
26
|
+
await super().cleanup()
|
autobyteus/llm/api/grok_llm.py
CHANGED
|
@@ -1,187 +1,26 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import
|
|
3
|
-
from typing import Optional, List, AsyncGenerator
|
|
4
|
-
from openai import OpenAI
|
|
5
|
-
from openai.types.completion_usage import CompletionUsage
|
|
6
|
-
from openai.types.chat import ChatCompletionChunk
|
|
7
|
-
from autobyteus.llm.base_llm import BaseLLM
|
|
2
|
+
from typing import Optional
|
|
8
3
|
from autobyteus.llm.models import LLMModel
|
|
9
4
|
from autobyteus.llm.utils.llm_config import LLMConfig
|
|
10
|
-
from autobyteus.llm.
|
|
11
|
-
from autobyteus.llm.utils.image_payload_formatter import process_image
|
|
12
|
-
from autobyteus.llm.utils.token_usage import TokenUsage
|
|
13
|
-
from autobyteus.llm.utils.response_types import CompleteResponse, ChunkResponse
|
|
5
|
+
from autobyteus.llm.api.openai_compatible_llm import OpenAICompatibleLLM
|
|
14
6
|
|
|
15
7
|
logger = logging.getLogger(__name__)
|
|
16
8
|
|
|
17
|
-
class GrokLLM(
|
|
9
|
+
class GrokLLM(OpenAICompatibleLLM):
|
|
18
10
|
def __init__(self, model: LLMModel = None, llm_config: LLMConfig = None):
|
|
19
|
-
grok_api_key = os.getenv("GROK_API_KEY")
|
|
20
|
-
if not grok_api_key:
|
|
21
|
-
logger.error("GROK_API_KEY environment variable is not set.")
|
|
22
|
-
raise ValueError("GROK_API_KEY environment variable is not set.")
|
|
23
|
-
|
|
24
|
-
self.client = OpenAI(api_key=grok_api_key, base_url="https://api.x.ai/v1")
|
|
25
|
-
logger.info("Grok API key and base URL set successfully")
|
|
26
|
-
|
|
27
11
|
# Provide defaults if not specified
|
|
28
12
|
if model is None:
|
|
29
|
-
model = LLMModel
|
|
13
|
+
model = LLMModel['grok-2-1212']
|
|
30
14
|
if llm_config is None:
|
|
31
15
|
llm_config = LLMConfig()
|
|
32
16
|
|
|
33
|
-
super().__init__(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if not usage_data:
|
|
39
|
-
return None
|
|
40
|
-
|
|
41
|
-
return TokenUsage(
|
|
42
|
-
prompt_tokens=usage_data.prompt_tokens,
|
|
43
|
-
completion_tokens=usage_data.completion_tokens,
|
|
44
|
-
total_tokens=usage_data.total_tokens
|
|
17
|
+
super().__init__(
|
|
18
|
+
model=model,
|
|
19
|
+
llm_config=llm_config,
|
|
20
|
+
api_key_env_var="GROK_API_KEY",
|
|
21
|
+
base_url="https://api.x.ai/v1"
|
|
45
22
|
)
|
|
23
|
+
logger.info(f"GrokLLM initialized with model: {self.model}")
|
|
46
24
|
|
|
47
|
-
async def _send_user_message_to_llm(
|
|
48
|
-
self, user_message: str, image_urls: Optional[List[str]] = None, **kwargs
|
|
49
|
-
) -> CompleteResponse:
|
|
50
|
-
"""
|
|
51
|
-
Sends a non-streaming request to the Grok API.
|
|
52
|
-
"""
|
|
53
|
-
content = []
|
|
54
|
-
|
|
55
|
-
if user_message:
|
|
56
|
-
content.append({"type": "text", "text": user_message})
|
|
57
|
-
|
|
58
|
-
if image_urls:
|
|
59
|
-
for image_url in image_urls:
|
|
60
|
-
try:
|
|
61
|
-
image_content = process_image(image_url)
|
|
62
|
-
content.append(image_content)
|
|
63
|
-
logger.info(f"Processed image: {image_url}")
|
|
64
|
-
except ValueError as e:
|
|
65
|
-
logger.error(f"Error processing image {image_url}: {str(e)}")
|
|
66
|
-
continue
|
|
67
|
-
|
|
68
|
-
self.add_user_message(content)
|
|
69
|
-
logger.debug(f"Prepared message content: {content}")
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
logger.info("Sending request to Grok API")
|
|
73
|
-
response = self.client.chat.completions.create(
|
|
74
|
-
model=self.model.value,
|
|
75
|
-
messages=[msg.to_dict() for msg in self.messages],
|
|
76
|
-
max_tokens=self.max_tokens,
|
|
77
|
-
)
|
|
78
|
-
full_message = response.choices[0].message
|
|
79
|
-
|
|
80
|
-
# Extract reasoning_content if present
|
|
81
|
-
reasoning = None
|
|
82
|
-
if hasattr(full_message, "reasoning_content") and full_message.reasoning_content:
|
|
83
|
-
reasoning = full_message.reasoning_content
|
|
84
|
-
elif "reasoning_content" in full_message and full_message["reasoning_content"]:
|
|
85
|
-
reasoning = full_message["reasoning_content"]
|
|
86
|
-
|
|
87
|
-
# Extract main content
|
|
88
|
-
main_content = ""
|
|
89
|
-
if hasattr(full_message, "content") and full_message.content:
|
|
90
|
-
main_content = full_message.content
|
|
91
|
-
elif "content" in full_message and full_message["content"]:
|
|
92
|
-
main_content = full_message["content"]
|
|
93
|
-
|
|
94
|
-
self.add_assistant_message(main_content, reasoning_content=reasoning)
|
|
95
|
-
|
|
96
|
-
token_usage = self._create_token_usage(response.usage)
|
|
97
|
-
logger.info("Received response from Grok API with usage data")
|
|
98
|
-
|
|
99
|
-
return CompleteResponse(
|
|
100
|
-
content=main_content,
|
|
101
|
-
reasoning=reasoning,
|
|
102
|
-
usage=token_usage
|
|
103
|
-
)
|
|
104
|
-
except Exception as e:
|
|
105
|
-
logger.error(f"Error in Grok API request: {str(e)}")
|
|
106
|
-
raise ValueError(f"Error in Grok API request: {str(e)}")
|
|
107
|
-
|
|
108
|
-
async def _stream_user_message_to_llm(
|
|
109
|
-
self, user_message: str, image_urls: Optional[List[str]] = None, **kwargs
|
|
110
|
-
) -> AsyncGenerator[ChunkResponse, None]:
|
|
111
|
-
"""
|
|
112
|
-
Streams the response from the Grok API.
|
|
113
|
-
Yields reasoning and content in separate chunks.
|
|
114
|
-
"""
|
|
115
|
-
content = []
|
|
116
|
-
|
|
117
|
-
if user_message:
|
|
118
|
-
content.append({"type": "text", "text": user_message})
|
|
119
|
-
|
|
120
|
-
if image_urls:
|
|
121
|
-
for image_url in image_urls:
|
|
122
|
-
try:
|
|
123
|
-
image_content = process_image(image_url)
|
|
124
|
-
content.append(image_content)
|
|
125
|
-
logger.info(f"Processed image for streaming: {image_url}")
|
|
126
|
-
except ValueError as e:
|
|
127
|
-
logger.error(f"Error processing image for streaming {image_url}: {str(e)}")
|
|
128
|
-
continue
|
|
129
|
-
|
|
130
|
-
self.add_user_message(content)
|
|
131
|
-
logger.debug(f"Prepared streaming message content: {content}")
|
|
132
|
-
|
|
133
|
-
# Initialize variables to track reasoning and main content
|
|
134
|
-
accumulated_reasoning = ""
|
|
135
|
-
accumulated_content = ""
|
|
136
|
-
|
|
137
|
-
try:
|
|
138
|
-
logger.info("Starting streaming request to Grok API")
|
|
139
|
-
stream = self.client.chat.completions.create(
|
|
140
|
-
model=self.model.value,
|
|
141
|
-
messages=[msg.to_dict() for msg in self.messages],
|
|
142
|
-
max_tokens=self.max_tokens,
|
|
143
|
-
stream=True,
|
|
144
|
-
stream_options={"include_usage": True}
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
for chunk in stream:
|
|
148
|
-
chunk: ChatCompletionChunk
|
|
149
|
-
|
|
150
|
-
# Process reasoning tokens
|
|
151
|
-
reasoning_chunk = getattr(chunk.choices[0].delta, "reasoning_content", None)
|
|
152
|
-
if reasoning_chunk:
|
|
153
|
-
accumulated_reasoning += reasoning_chunk
|
|
154
|
-
yield ChunkResponse(
|
|
155
|
-
content="",
|
|
156
|
-
reasoning=reasoning_chunk
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Process main content tokens
|
|
160
|
-
main_token = chunk.choices[0].delta.content
|
|
161
|
-
if main_token:
|
|
162
|
-
accumulated_content += main_token
|
|
163
|
-
yield ChunkResponse(
|
|
164
|
-
content=main_token,
|
|
165
|
-
reasoning=None
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# Yield token usage if available in the final chunk
|
|
169
|
-
if hasattr(chunk, "usage") and chunk.usage is not None:
|
|
170
|
-
token_usage = self._create_token_usage(chunk.usage)
|
|
171
|
-
yield ChunkResponse(
|
|
172
|
-
content="",
|
|
173
|
-
reasoning=None,
|
|
174
|
-
is_complete=True,
|
|
175
|
-
usage=token_usage
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
# After streaming, add the fully accumulated assistant message to history
|
|
179
|
-
self.add_assistant_message(accumulated_content, reasoning_content=accumulated_reasoning)
|
|
180
|
-
logger.info("Completed streaming response from Grok API")
|
|
181
|
-
|
|
182
|
-
except Exception as e:
|
|
183
|
-
logger.error(f"Error in Grok API streaming: {str(e)}")
|
|
184
|
-
raise ValueError(f"Error in Grok API streaming: {str(e)}")
|
|
185
|
-
|
|
186
25
|
async def cleanup(self):
|
|
187
|
-
await super().cleanup()
|
|
26
|
+
await super().cleanup()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from autobyteus.llm.models import LLMModel
|
|
4
|
+
from autobyteus.llm.utils.llm_config import LLMConfig
|
|
5
|
+
from autobyteus.llm.api.openai_compatible_llm import OpenAICompatibleLLM
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class KimiLLM(OpenAICompatibleLLM):
|
|
10
|
+
def __init__(self, model: LLMModel = None, llm_config: LLMConfig = None):
|
|
11
|
+
# Provide defaults if not specified
|
|
12
|
+
if model is None:
|
|
13
|
+
# Setting a default Kimi model from the factory ones
|
|
14
|
+
model = LLMModel['kimi-latest']
|
|
15
|
+
if llm_config is None:
|
|
16
|
+
llm_config = LLMConfig()
|
|
17
|
+
|
|
18
|
+
super().__init__(
|
|
19
|
+
model=model,
|
|
20
|
+
llm_config=llm_config,
|
|
21
|
+
api_key_env_var="KIMI_API_KEY",
|
|
22
|
+
base_url="https://api.moonshot.cn/v1"
|
|
23
|
+
)
|
|
24
|
+
logger.info(f"KimiLLM initialized with model: {self.model}")
|
|
@@ -16,7 +16,7 @@ class MistralLLM(BaseLLM):
|
|
|
16
16
|
def __init__(self, model: LLMModel = None, llm_config: LLMConfig = None):
|
|
17
17
|
# Provide defaults if not specified
|
|
18
18
|
if model is None:
|
|
19
|
-
model = LLMModel.
|
|
19
|
+
model = LLMModel.mistral_large
|
|
20
20
|
if llm_config is None:
|
|
21
21
|
llm_config = LLMConfig()
|
|
22
22
|
|
|
@@ -60,7 +60,7 @@ class MistralLLM(BaseLLM):
|
|
|
60
60
|
messages=mistral_messages,
|
|
61
61
|
)
|
|
62
62
|
|
|
63
|
-
assistant_message = chat_response.choices
|
|
63
|
+
assistant_message = chat_response.choices.message.content
|
|
64
64
|
self.add_assistant_message(assistant_message)
|
|
65
65
|
|
|
66
66
|
# Create token usage if available
|
|
@@ -93,8 +93,8 @@ class MistralLLM(BaseLLM):
|
|
|
93
93
|
accumulated_message = ""
|
|
94
94
|
|
|
95
95
|
async for chunk in stream:
|
|
96
|
-
if chunk.data.choices
|
|
97
|
-
token = chunk.data.choices
|
|
96
|
+
if chunk.data.choices.delta.content is not None:
|
|
97
|
+
token = chunk.data.choices.delta.content
|
|
98
98
|
accumulated_message += token
|
|
99
99
|
|
|
100
100
|
# For intermediate chunks, yield without usage
|
autobyteus/llm/api/ollama_llm.py
CHANGED
|
@@ -17,8 +17,8 @@ class OllamaLLM(BaseLLM):
|
|
|
17
17
|
DEFAULT_OLLAMA_HOST = 'http://localhost:11434'
|
|
18
18
|
|
|
19
19
|
def __init__(self, model: LLMModel = None, llm_config: LLMConfig = None):
|
|
20
|
-
self.ollama_host = os.getenv('
|
|
21
|
-
|
|
20
|
+
self.ollama_host = os.getenv('DEFAULT_OLLAMA_HOST', self.DEFAULT_OLLAMA_HOST)
|
|
21
|
+
logger.info(f"Initializing Ollama with host: {self.ollama_host}")
|
|
22
22
|
|
|
23
23
|
self.client = AsyncClient(host=self.ollama_host)
|
|
24
24
|
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from typing import Optional, List, AsyncGenerator
|
|
5
|
+
from openai import OpenAI
|
|
6
|
+
from openai.types.completion_usage import CompletionUsage
|
|
7
|
+
from openai.types.chat import ChatCompletionChunk
|
|
8
|
+
|
|
9
|
+
from autobyteus.llm.base_llm import BaseLLM
|
|
10
|
+
from autobyteus.llm.models import LLMModel
|
|
11
|
+
from autobyteus.llm.utils.llm_config import LLMConfig
|
|
12
|
+
from autobyteus.llm.utils.image_payload_formatter import process_image
|
|
13
|
+
from autobyteus.llm.utils.token_usage import TokenUsage
|
|
14
|
+
from autobyteus.llm.utils.response_types import CompleteResponse, ChunkResponse
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class OpenAICompatibleLLM(BaseLLM, ABC):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
model: LLMModel,
|
|
22
|
+
llm_config: LLMConfig,
|
|
23
|
+
api_key_env_var: str,
|
|
24
|
+
base_url: str
|
|
25
|
+
):
|
|
26
|
+
api_key = os.getenv(api_key_env_var)
|
|
27
|
+
if not api_key:
|
|
28
|
+
logger.error(f"{api_key_env_var} environment variable is not set.")
|
|
29
|
+
raise ValueError(f"{api_key_env_var} environment variable is not set.")
|
|
30
|
+
|
|
31
|
+
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
|
32
|
+
logger.info(f"Initialized OpenAI compatible client with base_url: {base_url}")
|
|
33
|
+
|
|
34
|
+
super().__init__(model=model, llm_config=llm_config)
|
|
35
|
+
self.max_tokens = 8000 # A default, can be overridden by subclass or config
|
|
36
|
+
|
|
37
|
+
def _create_token_usage(self, usage_data: Optional[CompletionUsage]) -> Optional[TokenUsage]:
|
|
38
|
+
"""Convert usage data to TokenUsage format."""
|
|
39
|
+
if not usage_data:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
return TokenUsage(
|
|
43
|
+
prompt_tokens=usage_data.prompt_tokens,
|
|
44
|
+
completion_tokens=usage_data.completion_tokens,
|
|
45
|
+
total_tokens=usage_data.total_tokens
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def _send_user_message_to_llm(
|
|
49
|
+
self, user_message: str, image_urls: Optional[List[str]] = None, **kwargs
|
|
50
|
+
) -> CompleteResponse:
|
|
51
|
+
"""
|
|
52
|
+
Sends a non-streaming request to an OpenAI-compatible API.
|
|
53
|
+
Supports optional reasoning content if provided in the response.
|
|
54
|
+
"""
|
|
55
|
+
content = []
|
|
56
|
+
|
|
57
|
+
if user_message:
|
|
58
|
+
content.append({"type": "text", "text": user_message})
|
|
59
|
+
|
|
60
|
+
if image_urls:
|
|
61
|
+
for image_url in image_urls:
|
|
62
|
+
try:
|
|
63
|
+
image_content = process_image(image_url)
|
|
64
|
+
content.append(image_content)
|
|
65
|
+
logger.info(f"Processed image: {image_url}")
|
|
66
|
+
except ValueError as e:
|
|
67
|
+
logger.error(f"Error processing image {image_url}: {str(e)}")
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
self.add_user_message(content)
|
|
71
|
+
logger.debug(f"Prepared message content: {content}")
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
logger.info(f"Sending request to {self.model.provider.value} API")
|
|
75
|
+
response = self.client.chat.completions.create(
|
|
76
|
+
model=self.model.value,
|
|
77
|
+
messages=[msg.to_dict() for msg in self.messages],
|
|
78
|
+
max_tokens=self.max_tokens,
|
|
79
|
+
)
|
|
80
|
+
full_message = response.choices[0].message
|
|
81
|
+
|
|
82
|
+
# Extract reasoning_content if present
|
|
83
|
+
reasoning = None
|
|
84
|
+
if hasattr(full_message, "reasoning_content") and full_message.reasoning_content:
|
|
85
|
+
reasoning = full_message.reasoning_content
|
|
86
|
+
elif "reasoning_content" in full_message and full_message["reasoning_content"]:
|
|
87
|
+
reasoning = full_message["reasoning_content"]
|
|
88
|
+
|
|
89
|
+
# Extract main content
|
|
90
|
+
main_content = ""
|
|
91
|
+
if hasattr(full_message, "content") and full_message.content:
|
|
92
|
+
main_content = full_message.content
|
|
93
|
+
elif "content" in full_message and full_message["content"]:
|
|
94
|
+
main_content = full_message["content"]
|
|
95
|
+
|
|
96
|
+
self.add_assistant_message(main_content, reasoning_content=reasoning)
|
|
97
|
+
|
|
98
|
+
token_usage = self._create_token_usage(response.usage)
|
|
99
|
+
logger.info(f"Received response from {self.model.provider.value} API with usage data")
|
|
100
|
+
|
|
101
|
+
return CompleteResponse(
|
|
102
|
+
content=main_content,
|
|
103
|
+
reasoning=reasoning,
|
|
104
|
+
usage=token_usage
|
|
105
|
+
)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Error in {self.model.provider.value} API request: {str(e)}")
|
|
108
|
+
raise ValueError(f"Error in {self.model.provider.value} API request: {str(e)}")
|
|
109
|
+
|
|
110
|
+
async def _stream_user_message_to_llm(
|
|
111
|
+
self, user_message: str, image_urls: Optional[List[str]] = None, **kwargs
|
|
112
|
+
) -> AsyncGenerator[ChunkResponse, None]:
|
|
113
|
+
"""
|
|
114
|
+
Streams the response from an OpenAI-compatible API.
|
|
115
|
+
Yields reasoning and content in separate chunks.
|
|
116
|
+
"""
|
|
117
|
+
content = []
|
|
118
|
+
|
|
119
|
+
if user_message:
|
|
120
|
+
content.append({"type": "text", "text": user_message})
|
|
121
|
+
|
|
122
|
+
if image_urls:
|
|
123
|
+
for image_url in image_urls:
|
|
124
|
+
try:
|
|
125
|
+
image_content = process_image(image_url)
|
|
126
|
+
content.append(image_content)
|
|
127
|
+
logger.info(f"Processed image for streaming: {image_url}")
|
|
128
|
+
except ValueError as e:
|
|
129
|
+
logger.error(f"Error processing image for streaming {image_url}: {str(e)}")
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
self.add_user_message(content)
|
|
133
|
+
logger.debug(f"Prepared streaming message content: {content}")
|
|
134
|
+
|
|
135
|
+
# Initialize variables to track reasoning and main content
|
|
136
|
+
accumulated_reasoning = ""
|
|
137
|
+
accumulated_content = ""
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
logger.info(f"Starting streaming request to {self.model.provider.value} API")
|
|
141
|
+
stream = self.client.chat.completions.create(
|
|
142
|
+
model=self.model.value,
|
|
143
|
+
messages=[msg.to_dict() for msg in self.messages],
|
|
144
|
+
max_tokens=self.max_tokens,
|
|
145
|
+
stream=True,
|
|
146
|
+
stream_options={"include_usage": True}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
for chunk in stream:
|
|
150
|
+
chunk: ChatCompletionChunk
|
|
151
|
+
if not chunk.choices:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
delta = chunk.choices[0].delta
|
|
155
|
+
|
|
156
|
+
# Process reasoning tokens (if supported by model)
|
|
157
|
+
reasoning_chunk = getattr(delta, "reasoning_content", None)
|
|
158
|
+
if reasoning_chunk:
|
|
159
|
+
accumulated_reasoning += reasoning_chunk
|
|
160
|
+
yield ChunkResponse(
|
|
161
|
+
content="",
|
|
162
|
+
reasoning=reasoning_chunk
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Process main content tokens
|
|
166
|
+
main_token = delta.content
|
|
167
|
+
if main_token:
|
|
168
|
+
accumulated_content += main_token
|
|
169
|
+
yield ChunkResponse(
|
|
170
|
+
content=main_token,
|
|
171
|
+
reasoning=None
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Yield token usage if available in the final chunk
|
|
175
|
+
if hasattr(chunk, "usage") and chunk.usage is not None:
|
|
176
|
+
token_usage = self._create_token_usage(chunk.usage)
|
|
177
|
+
yield ChunkResponse(
|
|
178
|
+
content="",
|
|
179
|
+
reasoning=None,
|
|
180
|
+
is_complete=True,
|
|
181
|
+
usage=token_usage
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# After streaming, add the fully accumulated assistant message to history
|
|
185
|
+
self.add_assistant_message(accumulated_content, reasoning_content=accumulated_reasoning)
|
|
186
|
+
logger.info(f"Completed streaming response from {self.model.provider.value} API")
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Error in {self.model.provider.value} API streaming: {str(e)}")
|
|
190
|
+
raise ValueError(f"Error in {self.model.provider.value} API streaming: {str(e)}")
|
|
191
|
+
|
|
192
|
+
async def cleanup(self):
|
|
193
|
+
await super().cleanup()
|