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:
Zane Chen
2026-05-19 20:16:37 -04:00
committed by GitHub
parent f45e79af53
commit 8e71c10f13
83 changed files with 732 additions and 210 deletions
@@ -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
View File
@@ -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
```
+16 -5
View File
@@ -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
+2 -2
View File
@@ -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"]
+18 -3
View File
@@ -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,
)
+16 -5
View File
@@ -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
+2 -2
View File
@@ -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"]
+18 -3
View File
@@ -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,
)
+16 -5
View File
@@ -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"]
+18 -3
View File
@@ -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,
)
+16 -5
View File
@@ -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,
)
+16 -5
View File
@@ -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"]
+18 -3
View File
@@ -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,
)
+16 -5
View File
@@ -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
+1 -1
View File
@@ -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"]
+18 -3
View File
@@ -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
View File
@@ -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)
+16 -5
View File
@@ -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"]
+18 -3
View File
@@ -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
View File
@@ -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)
+16 -5
View File
@@ -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,
)
+1 -1
View File
@@ -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)
+16 -5
View File
@@ -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
+1 -1
View File
@@ -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"]
+18 -3
View File
@@ -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
View File
@@ -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)
+16 -5
View File
@@ -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
+1 -1
View File
@@ -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"]
+18 -3
View File
@@ -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
View File
@@ -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)
+16 -5
View File
@@ -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,
)
+1 -1
View File
@@ -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)
+16 -5
View File
@@ -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,
)
+1 -1
View File
@@ -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)
+16 -5
View File
@@ -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,
)
+1 -1
View File
@@ -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)
+16 -5
View File
@@ -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,
)
+1 -1
View File
@@ -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)
+16 -5
View File
@@ -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
View File
@@ -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)
+16 -5
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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)
+16 -5
View File
@@ -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
+1 -1
View File
@@ -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
)
+16 -2
View File
@@ -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()
+2 -2
View File
@@ -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"]
+18 -3
View File
@@ -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,
)