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,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
- **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
|
||||
- **Tool Calling Loop**: Agent calls tools, adds results to history, continues conversation
|
||||
|
||||
@@ -59,10 +59,11 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
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 = {
|
||||
"role": "assistant",
|
||||
@@ -71,10 +72,17 @@ class AgentSession:
|
||||
}
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
```
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -65,10 +66,11 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
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] = [
|
||||
{
|
||||
@@ -86,12 +88,21 @@ class AgentSession:
|
||||
assistant_msg["tool_calls"] = tool_call_dicts
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -81,10 +82,11 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
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] = [
|
||||
{
|
||||
@@ -102,12 +104,21 @@ class AgentSession:
|
||||
assistant_msg["tool_calls"] = tool_call_dicts
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -83,10 +84,11 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
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] = [
|
||||
{
|
||||
@@ -104,12 +106,21 @@ class AgentSession:
|
||||
assistant_msg["tool_calls"] = tool_call_dicts
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -91,10 +92,11 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
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] = [
|
||||
{
|
||||
@@ -112,12 +114,21 @@ class AgentSession:
|
||||
assistant_msg["tool_calls"] = tool_call_dicts
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -104,13 +105,14 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
|
||||
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] = [
|
||||
{
|
||||
@@ -128,12 +130,21 @@ class AgentSession:
|
||||
assistant_msg["tool_calls"] = tool_call_dicts
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -115,13 +116,14 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
|
||||
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] = [
|
||||
{
|
||||
@@ -139,12 +141,21 @@ class AgentSession:
|
||||
assistant_msg["tool_calls"] = tool_call_dicts
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -93,7 +93,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -157,11 +158,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -180,12 +182,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -157,11 +158,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -180,12 +182,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -168,11 +169,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -191,12 +193,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -168,11 +169,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -191,12 +193,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -168,11 +169,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -191,12 +193,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -168,11 +169,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -191,12 +193,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -168,11 +169,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -191,12 +193,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
||||
|
||||
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:
|
||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||
parts = []
|
||||
|
||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||
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"
|
||||
if agents_path.exists():
|
||||
parts.append(agents_path.read_text().strip())
|
||||
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||
|
||||
# Dynamic cron list
|
||||
cron_list = self._format_cron_list()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -177,11 +178,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -200,12 +202,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
||||
|
||||
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:
|
||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||
parts = []
|
||||
|
||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||
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"
|
||||
if agents_path.exists():
|
||||
parts.append(agents_path.read_text().strip())
|
||||
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||
|
||||
# Dynamic cron list
|
||||
cron_list = self._format_cron_list()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -185,11 +186,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -208,12 +210,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
||||
|
||||
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:
|
||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||
parts = []
|
||||
|
||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||
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"
|
||||
if agents_path.exists():
|
||||
parts.append(agents_path.read_text().strip())
|
||||
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||
|
||||
# Dynamic cron list
|
||||
cron_list = self._format_cron_list()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -185,11 +186,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -208,12 +210,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
||||
|
||||
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:
|
||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||
parts = []
|
||||
|
||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||
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"
|
||||
if agents_path.exists():
|
||||
parts.append(agents_path.read_text().strip())
|
||||
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||
|
||||
# Dynamic cron list
|
||||
cron_list = self._format_cron_list()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ cookie: Yes, <content>.
|
||||
cd 17-memory
|
||||
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.
|
||||
|
||||
uv run my-bot chat
|
||||
|
||||
@@ -100,7 +100,7 @@ class ChatLoop:
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -185,11 +186,12 @@ class AgentSession:
|
||||
self.state.add_message(user_msg)
|
||||
|
||||
tool_schemas = self.tools.get_tool_schemas()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
messages = self.state.build_messages()
|
||||
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] = [
|
||||
{
|
||||
@@ -208,12 +210,21 @@ class AgentSession:
|
||||
|
||||
self.state.add_message(assistant_msg)
|
||||
|
||||
if not tool_calls:
|
||||
break
|
||||
if stop_reason == "tool_calls":
|
||||
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
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class ContextGuard:
|
||||
|
||||
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}],
|
||||
[], # No tools needed
|
||||
)
|
||||
|
||||
@@ -45,17 +45,31 @@ class PromptBuilder:
|
||||
|
||||
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:
|
||||
"""Load BOOTSTRAP.md + AGENTS.md + cron list."""
|
||||
parts = []
|
||||
|
||||
bootstrap_path = self.context.config.workspace / "BOOTSTRAP.md"
|
||||
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"
|
||||
if agents_path.exists():
|
||||
parts.append(agents_path.read_text().strip())
|
||||
parts.append(self._substitute_paths(agents_path.read_text().strip()))
|
||||
|
||||
# Dynamic cron list
|
||||
cron_list = self._format_cron_list()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""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.types.completion import ChatCompletionMessageParam as Message
|
||||
from litellm.types.utils import OpenAIChatCompletionFinishReason
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mybot.utils.config import LLMConfig
|
||||
|
||||
StopReason = OpenAIChatCompletionFinishReason
|
||||
|
||||
|
||||
@dataclass
|
||||
class LLMToolCall:
|
||||
@@ -55,12 +58,21 @@ class LLMProvider:
|
||||
messages: list[Message],
|
||||
tools: Optional[list[dict[str, Any]]] = None,
|
||||
**kwargs: Any,
|
||||
) -> tuple[str, list[LLMToolCall]]:
|
||||
"""Default implementation using litellm. Subclasses can override."""
|
||||
) -> tuple[str, list[LLMToolCall], StopReason]:
|
||||
"""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] = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"api_key": self.api_key,
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
}
|
||||
|
||||
if self.api_base:
|
||||
@@ -71,7 +83,9 @@ class LLMProvider:
|
||||
|
||||
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 (
|
||||
message.content or "",
|
||||
@@ -83,4 +97,5 @@ class LLMProvider:
|
||||
)
|
||||
for tc in (message.tool_calls or [])
|
||||
],
|
||||
stop_reason,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user