mirror of
https://github.com/czl9707/build-your-own-openclaw.git
synced 2026-08-14 00:47:59 +00:00
chore: respecting stop reason (#17)
* chore: respecting stop reason * fix: add back template substitution * typo fix * increase timeout allow slower model
This commit is contained in:
@@ -50,6 +50,8 @@ class LLMProvider:
|
|||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
|
|||||||
+13
-5
@@ -19,7 +19,7 @@ Giving the agent the ability to actually *do* things, from chatting only to taki
|
|||||||
|
|
||||||
## Key Components
|
## Key Components
|
||||||
|
|
||||||
- **Stop Reason**: Chat Loop can stop because of "end_turn" or "tool_use"
|
- **Stop Reason**: Chat loop branches on `stop_reason` — `"tool_calls"` to execute tools, `"stop"` for normal completion, `"length"` for truncated responses
|
||||||
- **Tools**: Manages available tools and executes tool calls
|
- **Tools**: Manages available tools and executes tool calls
|
||||||
- **Tool Calling Loop**: Agent calls tools, adds results to history, continues conversation
|
- **Tool Calling Loop**: Agent calls tools, adds results to history, continues conversation
|
||||||
|
|
||||||
@@ -59,10 +59,11 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
assistant_msg: Message = {
|
assistant_msg: Message = {
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
@@ -71,10 +72,17 @@ class AgentSession:
|
|||||||
}
|
}
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -65,10 +66,11 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -86,12 +88,21 @@ class AgentSession:
|
|||||||
assistant_msg["tool_calls"] = tool_call_dicts
|
assistant_msg["tool_calls"] = tool_call_dicts
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -81,10 +82,11 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -102,12 +104,21 @@ class AgentSession:
|
|||||||
assistant_msg["tool_calls"] = tool_call_dicts
|
assistant_msg["tool_calls"] = tool_call_dicts
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -83,10 +84,11 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -104,12 +106,21 @@ class AgentSession:
|
|||||||
assistant_msg["tool_calls"] = tool_call_dicts
|
assistant_msg["tool_calls"] = tool_call_dicts
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -91,10 +92,11 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -112,12 +114,21 @@ class AgentSession:
|
|||||||
assistant_msg["tool_calls"] = tool_call_dicts
|
assistant_msg["tool_calls"] = tool_call_dicts
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -104,13 +105,14 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
|
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
|
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -128,12 +130,21 @@ class AgentSession:
|
|||||||
assistant_msg["tool_calls"] = tool_call_dicts
|
assistant_msg["tool_calls"] = tool_call_dicts
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -115,13 +116,14 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
|
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
|
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -139,12 +141,21 @@ class AgentSession:
|
|||||||
assistant_msg["tool_calls"] = tool_call_dicts
|
assistant_msg["tool_calls"] = tool_call_dicts
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -157,11 +158,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -180,12 +182,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -157,11 +158,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -180,12 +182,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -168,11 +169,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -191,12 +193,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -168,11 +169,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -191,12 +193,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -168,11 +169,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -191,12 +193,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -168,11 +169,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -191,12 +193,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -168,11 +169,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -191,12 +193,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
|||||||
|
|
||||||
return "\n\n".join(layers)
|
return "\n\n".join(layers)
|
||||||
|
|
||||||
|
def _substitute_paths(self, text: str) -> str:
|
||||||
|
"""Replace {{placeholder}} tokens with resolved config paths."""
|
||||||
|
cfg = self.context.config
|
||||||
|
replacements = {
|
||||||
|
"{{workspace}}": str(cfg.workspace),
|
||||||
|
"{{skills_path}}": str(cfg.skills_path),
|
||||||
|
"{{crons_path}}": str(cfg.crons_path),
|
||||||
|
"{{memories_path}}": str(cfg.memories_path),
|
||||||
|
"{{agents_path}}": str(cfg.agents_path),
|
||||||
|
}
|
||||||
|
for placeholder, value in replacements.items():
|
||||||
|
text = text.replace(placeholder, value)
|
||||||
|
return text
|
||||||
|
|
||||||
def _load_bootstrap_context(self) -> str:
|
def _load_bootstrap_context(self) -> str:
|
||||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||||
if bootstrap_path.exists():
|
if bootstrap_path.exists():
|
||||||
parts.append(bootstrap_path.read_text().strip())
|
parts.append(self._substitute_paths(bootstrap_path.read_text().strip()))
|
||||||
|
|
||||||
agents_path = self.context.config.workspace / "AGENTS.md"
|
agents_path = self.context.config.workspace / "AGENTS.md"
|
||||||
if agents_path.exists():
|
if agents_path.exists():
|
||||||
parts.append(agents_path.read_text().strip())
|
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||||
|
|
||||||
# Dynamic cron list
|
# Dynamic cron list
|
||||||
cron_list = self._format_cron_list()
|
cron_list = self._format_cron_list()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -177,11 +178,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -200,12 +202,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
|||||||
|
|
||||||
return "\n\n".join(layers)
|
return "\n\n".join(layers)
|
||||||
|
|
||||||
|
def _substitute_paths(self, text: str) -> str:
|
||||||
|
"""Replace {{placeholder}} tokens with resolved config paths."""
|
||||||
|
cfg = self.context.config
|
||||||
|
replacements = {
|
||||||
|
"{{workspace}}": str(cfg.workspace),
|
||||||
|
"{{skills_path}}": str(cfg.skills_path),
|
||||||
|
"{{crons_path}}": str(cfg.crons_path),
|
||||||
|
"{{memories_path}}": str(cfg.memories_path),
|
||||||
|
"{{agents_path}}": str(cfg.agents_path),
|
||||||
|
}
|
||||||
|
for placeholder, value in replacements.items():
|
||||||
|
text = text.replace(placeholder, value)
|
||||||
|
return text
|
||||||
|
|
||||||
def _load_bootstrap_context(self) -> str:
|
def _load_bootstrap_context(self) -> str:
|
||||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||||
if bootstrap_path.exists():
|
if bootstrap_path.exists():
|
||||||
parts.append(bootstrap_path.read_text().strip())
|
parts.append(self._substitute_paths(bootstrap_path.read_text().strip()))
|
||||||
|
|
||||||
agents_path = self.context.config.workspace / "AGENTS.md"
|
agents_path = self.context.config.workspace / "AGENTS.md"
|
||||||
if agents_path.exists():
|
if agents_path.exists():
|
||||||
parts.append(agents_path.read_text().strip())
|
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||||
|
|
||||||
# Dynamic cron list
|
# Dynamic cron list
|
||||||
cron_list = self._format_cron_list()
|
cron_list = self._format_cron_list()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -185,11 +186,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -208,12 +210,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
|||||||
|
|
||||||
return "\n\n".join(layers)
|
return "\n\n".join(layers)
|
||||||
|
|
||||||
|
def _substitute_paths(self, text: str) -> str:
|
||||||
|
"""Replace {{placeholder}} tokens with resolved config paths."""
|
||||||
|
cfg = self.context.config
|
||||||
|
replacements = {
|
||||||
|
"{{workspace}}": str(cfg.workspace),
|
||||||
|
"{{skills_path}}": str(cfg.skills_path),
|
||||||
|
"{{crons_path}}": str(cfg.crons_path),
|
||||||
|
"{{memories_path}}": str(cfg.memories_path),
|
||||||
|
"{{agents_path}}": str(cfg.agents_path),
|
||||||
|
}
|
||||||
|
for placeholder, value in replacements.items():
|
||||||
|
text = text.replace(placeholder, value)
|
||||||
|
return text
|
||||||
|
|
||||||
def _load_bootstrap_context(self) -> str:
|
def _load_bootstrap_context(self) -> str:
|
||||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||||
if bootstrap_path.exists():
|
if bootstrap_path.exists():
|
||||||
parts.append(bootstrap_path.read_text().strip())
|
parts.append(self._substitute_paths(bootstrap_path.read_text().strip()))
|
||||||
|
|
||||||
agents_path = self.context.config.workspace / "AGENTS.md"
|
agents_path = self.context.config.workspace / "AGENTS.md"
|
||||||
if agents_path.exists():
|
if agents_path.exists():
|
||||||
parts.append(agents_path.read_text().strip())
|
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||||
|
|
||||||
# Dynamic cron list
|
# Dynamic cron list
|
||||||
cron_list = self._format_cron_list()
|
cron_list = self._format_cron_list()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -185,11 +186,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -208,12 +210,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
|||||||
|
|
||||||
return "\n\n".join(layers)
|
return "\n\n".join(layers)
|
||||||
|
|
||||||
|
def _substitute_paths(self, text: str) -> str:
|
||||||
|
"""Replace {{placeholder}} tokens with resolved config paths."""
|
||||||
|
cfg = self.context.config
|
||||||
|
replacements = {
|
||||||
|
"{{workspace}}": str(cfg.workspace),
|
||||||
|
"{{skills_path}}": str(cfg.skills_path),
|
||||||
|
"{{crons_path}}": str(cfg.crons_path),
|
||||||
|
"{{memories_path}}": str(cfg.memories_path),
|
||||||
|
"{{agents_path}}": str(cfg.agents_path),
|
||||||
|
}
|
||||||
|
for placeholder, value in replacements.items():
|
||||||
|
text = text.replace(placeholder, value)
|
||||||
|
return text
|
||||||
|
|
||||||
def _load_bootstrap_context(self) -> str:
|
def _load_bootstrap_context(self) -> str:
|
||||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||||
if bootstrap_path.exists():
|
if bootstrap_path.exists():
|
||||||
parts.append(bootstrap_path.read_text().strip())
|
parts.append(self._substitute_paths(bootstrap_path.read_text().strip()))
|
||||||
|
|
||||||
agents_path = self.context.config.workspace / "AGENTS.md"
|
agents_path = self.context.config.workspace / "AGENTS.md"
|
||||||
if agents_path.exists():
|
if agents_path.exists():
|
||||||
parts.append(agents_path.read_text().strip())
|
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||||
|
|
||||||
# Dynamic cron list
|
# Dynamic cron list
|
||||||
cron_list = self._format_cron_list()
|
cron_list = self._format_cron_list()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ cookie: Yes, <content>.
|
|||||||
cd 17-memory
|
cd 17-memory
|
||||||
uv run my-bot chat
|
uv run my-bot chat
|
||||||
|
|
||||||
# You: Remember that I my name is Zane
|
# You: Remember that my name is Zane
|
||||||
# Pickle: Got it! I've saved that preference.
|
# Pickle: Got it! I've saved that preference.
|
||||||
|
|
||||||
uv run my-bot chat
|
uv run my-bot chat
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
response = await asyncio.wait_for(
|
response = await asyncio.wait_for(
|
||||||
self.response_queue.get(), timeout=60.0
|
self.response_queue.get(), timeout=120.0
|
||||||
)
|
)
|
||||||
|
|
||||||
self.display_agent_response(response.content)
|
self.display_agent_response(response.content)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -185,11 +186,12 @@ class AgentSession:
|
|||||||
self.state.add_message(user_msg)
|
self.state.add_message(user_msg)
|
||||||
|
|
||||||
tool_schemas = self.tools.get_tool_schemas()
|
tool_schemas = self.tools.get_tool_schemas()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
messages = self.state.build_messages()
|
messages = self.state.build_messages()
|
||||||
self.state = await self.context_guard.check_and_compact(self.state)
|
self.state = await self.context_guard.check_and_compact(self.state)
|
||||||
content, tool_calls = await self.agent.llm.chat(messages, tool_schemas)
|
content, tool_calls, stop_reason = await self.agent.llm.chat(messages, tool_schemas)
|
||||||
|
|
||||||
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
tool_call_dicts: list[ChatCompletionMessageToolCallParam] = [
|
||||||
{
|
{
|
||||||
@@ -208,12 +210,21 @@ class AgentSession:
|
|||||||
|
|
||||||
self.state.add_message(assistant_msg)
|
self.state.add_message(assistant_msg)
|
||||||
|
|
||||||
if not tool_calls:
|
if stop_reason == "tool_calls":
|
||||||
break
|
await self._handle_tool_calls(tool_calls)
|
||||||
|
continue
|
||||||
|
|
||||||
await self._handle_tool_calls(tool_calls)
|
if stop_reason == "length":
|
||||||
|
logger.warning(
|
||||||
|
"LLM response truncated (max_tokens reached), "
|
||||||
|
"returning partial response"
|
||||||
|
)
|
||||||
|
|
||||||
continue
|
if stop_reason == "content_filter":
|
||||||
|
logger.warning("LLM response filtered by content filter")
|
||||||
|
return content if content else "I'm unable to respond to that request."
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
|||||||
|
|
||||||
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
summary_prompt = COMPACT_PROMPT.format(conversation=old_text)
|
||||||
|
|
||||||
response, _ = await state.agent.llm.chat(
|
response, _, _ = await state.agent.llm.chat(
|
||||||
[{"role": "user", "content": summary_prompt}],
|
[{"role": "user", "content": summary_prompt}],
|
||||||
[], # No tools needed
|
[], # No tools needed
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
|||||||
|
|
||||||
return "\n\n".join(layers)
|
return "\n\n".join(layers)
|
||||||
|
|
||||||
|
def _substitute_paths(self, text: str) -> str:
|
||||||
|
"""Replace {{placeholder}} tokens with resolved config paths."""
|
||||||
|
cfg = self.context.config
|
||||||
|
replacements = {
|
||||||
|
"{{workspace}}": str(cfg.workspace),
|
||||||
|
"{{skills_path}}": str(cfg.skills_path),
|
||||||
|
"{{crons_path}}": str(cfg.crons_path),
|
||||||
|
"{{memories_path}}": str(cfg.memories_path),
|
||||||
|
"{{agents_path}}": str(cfg.agents_path),
|
||||||
|
}
|
||||||
|
for placeholder, value in replacements.items():
|
||||||
|
text = text.replace(placeholder, value)
|
||||||
|
return text
|
||||||
|
|
||||||
def _load_bootstrap_context(self) -> str:
|
def _load_bootstrap_context(self) -> str:
|
||||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||||
if bootstrap_path.exists():
|
if bootstrap_path.exists():
|
||||||
parts.append(bootstrap_path.read_text().strip())
|
parts.append(self._substitute_paths(bootstrap_path.read_text().strip()))
|
||||||
|
|
||||||
agents_path = self.context.config.workspace / "AGENTS.md"
|
agents_path = self.context.config.workspace / "AGENTS.md"
|
||||||
if agents_path.exists():
|
if agents_path.exists():
|
||||||
parts.append(agents_path.read_text().strip())
|
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||||
|
|
||||||
# Dynamic cron list
|
# Dynamic cron list
|
||||||
cron_list = self._format_cron_list()
|
cron_list = self._format_cron_list()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""LLM provider abstraction."""
|
"""LLM provider abstraction."""
|
||||||
|
|
||||||
from .base import LLMProvider, LLMToolCall
|
from .base import LLMProvider, LLMToolCall, StopReason
|
||||||
|
|
||||||
__all__ = ["LLMProvider", "LLMToolCall"]
|
__all__ = ["LLMProvider", "LLMToolCall", "StopReason"]
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ from typing import Any, Optional, cast
|
|||||||
|
|
||||||
from litellm import acompletion, Choices, TYPE_CHECKING
|
from litellm import acompletion, Choices, TYPE_CHECKING
|
||||||
from litellm.types.completion import ChatCompletionMessageParam as Message
|
from litellm.types.completion import ChatCompletionMessageParam as Message
|
||||||
|
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mybot.utils.config import LLMConfig
|
from mybot.utils.config import LLMConfig
|
||||||
|
|
||||||
|
StopReason = OpenAIChatCompletionFinishReason
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMToolCall:
|
class LLMToolCall:
|
||||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
|||||||
messages: list[Message],
|
messages: list[Message],
|
||||||
tools: Optional[list[dict[str, Any]]] = None,
|
tools: Optional[list[dict[str, Any]]] = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> tuple[str, list[LLMToolCall]]:
|
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||||
"""Default implementation using litellm. Subclasses can override."""
|
"""Send a chat request to the LLM.
|
||||||
|
|
||||||
|
Default implementation using litellm. Subclasses can override
|
||||||
|
if provider-specific behavior is needed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (content, tool_calls, stop_reason)
|
||||||
|
"""
|
||||||
request_kwargs: dict[str, Any] = {
|
request_kwargs: dict[str, Any] = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": messages,
|
"messages": messages,
|
||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"max_tokens": self.max_tokens,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.api_base:
|
if self.api_base:
|
||||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
|||||||
|
|
||||||
response = await acompletion(**request_kwargs)
|
response = await acompletion(**request_kwargs)
|
||||||
|
|
||||||
message = cast(Choices, response.choices[0]).message
|
choice = cast(Choices, response.choices[0])
|
||||||
|
message = choice.message
|
||||||
|
stop_reason = choice.finish_reason
|
||||||
|
|
||||||
return (
|
return (
|
||||||
message.content or "",
|
message.content or "",
|
||||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
|||||||
)
|
)
|
||||||
for tc in (message.tool_calls or [])
|
for tc in (message.tool_calls or [])
|
||||||
],
|
],
|
||||||
|
stop_reason,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user