autochatlib 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autochat/__init__.py +66 -0
- autochat/chat.py +116 -0
- autochat/compression/__init__.py +11 -0
- autochat/compression/config.py +86 -0
- autochat/compression/strategies.py +186 -0
- autochat/compression/types.py +21 -0
- autochat/config.py +11 -0
- autochat/exceptions/__init__.py +3 -0
- autochat/exceptions/errors.py +6 -0
- autochat/graph/__init__.py +4 -0
- autochat/graph/builder.py +151 -0
- autochat/graph/retrievers.py +51 -0
- autochat/graph/runtime.py +34 -0
- autochat/graph/state.py +12 -0
- autochat/graph/tools.py +72 -0
- autochat/guidelines.py +6 -0
- autochat/py.typed +0 -0
- autochat/retrieval/__init__.py +23 -0
- autochat/retrieval/base.py +201 -0
- autochat/retrieval/config.py +14 -0
- autochat/retrieval/documents.py +17 -0
- autochat/retrieval/strategies.py +37 -0
- autochat/retrieval/types.py +48 -0
- autochat/runtime/__init__.py +3 -0
- autochat/runtime/context.py +18 -0
- autochat/tools/__init__.py +19 -0
- autochat/tools/base.py +326 -0
- autochat/tools/decorators.py +37 -0
- autochat/tools/types.py +48 -0
- autochatlib-0.1.0.dist-info/METADATA +350 -0
- autochatlib-0.1.0.dist-info/RECORD +33 -0
- autochatlib-0.1.0.dist-info/WHEEL +4 -0
- autochatlib-0.1.0.dist-info/licenses/LICENSE +21 -0
autochat/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from .chat import AutoChat
|
|
2
|
+
from .compression import (
|
|
3
|
+
AutoCompress,
|
|
4
|
+
CompressionStrategy,
|
|
5
|
+
KeepLatestN,
|
|
6
|
+
SummarizeAll,
|
|
7
|
+
SummarizeLatestN,
|
|
8
|
+
)
|
|
9
|
+
from .config import ChatConfig
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
AutoChatError,
|
|
12
|
+
AutoChatToolError,
|
|
13
|
+
)
|
|
14
|
+
from .guidelines import ChatGuideline
|
|
15
|
+
from .retrieval import (
|
|
16
|
+
ChatRetriever,
|
|
17
|
+
RAGStrategy,
|
|
18
|
+
RetrievedDocument,
|
|
19
|
+
RetrievalConfig,
|
|
20
|
+
RetrievalInvocation,
|
|
21
|
+
RetrievalResult,
|
|
22
|
+
RetrieverFn,
|
|
23
|
+
RetrieverPostprocessor,
|
|
24
|
+
RetrieverPreprocessor,
|
|
25
|
+
SimpleRAGStrategy,
|
|
26
|
+
)
|
|
27
|
+
from .runtime import (
|
|
28
|
+
ChatRuntime,
|
|
29
|
+
)
|
|
30
|
+
from .tools import (
|
|
31
|
+
ChatTool,
|
|
32
|
+
ToolInvocation,
|
|
33
|
+
ToolPostprocessor,
|
|
34
|
+
ToolPreprocessor,
|
|
35
|
+
chat_tool,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__version__ = "0.1.0"
|
|
39
|
+
__all__ = [
|
|
40
|
+
"AutoChat",
|
|
41
|
+
"AutoCompress",
|
|
42
|
+
"ChatConfig",
|
|
43
|
+
"CompressionStrategy",
|
|
44
|
+
"KeepLatestN",
|
|
45
|
+
"SummarizeAll",
|
|
46
|
+
"SummarizeLatestN",
|
|
47
|
+
"AutoChatError",
|
|
48
|
+
"AutoChatToolError",
|
|
49
|
+
"ChatRuntime",
|
|
50
|
+
"ChatRetriever",
|
|
51
|
+
"RetrievalConfig",
|
|
52
|
+
"RetrievedDocument",
|
|
53
|
+
"RetrievalResult",
|
|
54
|
+
"RAGStrategy",
|
|
55
|
+
"SimpleRAGStrategy",
|
|
56
|
+
"RetrievalInvocation",
|
|
57
|
+
"RetrieverFn",
|
|
58
|
+
"RetrieverPostprocessor",
|
|
59
|
+
"RetrieverPreprocessor",
|
|
60
|
+
"ChatTool",
|
|
61
|
+
"ToolInvocation",
|
|
62
|
+
"ToolPostprocessor",
|
|
63
|
+
"ToolPreprocessor",
|
|
64
|
+
"chat_tool",
|
|
65
|
+
"ChatGuideline",
|
|
66
|
+
]
|
autochat/chat.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import Any, AsyncIterator, Generic, Literal, Mapping, Sequence, TypeVar
|
|
2
|
+
from uuid import uuid4
|
|
3
|
+
|
|
4
|
+
from langchain_core.messages import HumanMessage
|
|
5
|
+
from langchain_core.runnables import RunnableConfig
|
|
6
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
7
|
+
|
|
8
|
+
from autochat.config import ChatConfig
|
|
9
|
+
from autochat.compression import AutoCompress
|
|
10
|
+
from autochat.graph.builder import build_chat_graph
|
|
11
|
+
from autochat.graph.runtime import with_runtime_config
|
|
12
|
+
from autochat.guidelines import ChatGuideline
|
|
13
|
+
from autochat.retrieval import ChatRetriever, RetrievalConfig
|
|
14
|
+
from autochat.runtime import ChatRuntime
|
|
15
|
+
from autochat.tools import ChatTool
|
|
16
|
+
|
|
17
|
+
TContext = TypeVar("TContext")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AutoChat(Generic[TContext]):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
config: ChatConfig,
|
|
25
|
+
tools: Sequence[ChatTool[TContext, Any, Any]] = (),
|
|
26
|
+
retrievers: Sequence[ChatRetriever[TContext]] = (),
|
|
27
|
+
retrieval: RetrievalConfig[TContext] | None = None,
|
|
28
|
+
persistence: BaseCheckpointSaver | None = None,
|
|
29
|
+
compression: AutoCompress[TContext] | None = None,
|
|
30
|
+
system_message: str | None = None,
|
|
31
|
+
guidelines: Sequence[ChatGuideline] = (),
|
|
32
|
+
) -> None:
|
|
33
|
+
self.config = config
|
|
34
|
+
self.tools = tuple(tools)
|
|
35
|
+
self.retrievers = tuple(retrievers)
|
|
36
|
+
self.retrieval = retrieval or RetrievalConfig()
|
|
37
|
+
self.system_message = system_message
|
|
38
|
+
self.guidelines = tuple(guidelines)
|
|
39
|
+
self.persistence = persistence
|
|
40
|
+
self.compression = compression
|
|
41
|
+
|
|
42
|
+
self._graph = build_chat_graph(
|
|
43
|
+
config=config,
|
|
44
|
+
tools=self.tools,
|
|
45
|
+
retrievers=self.retrievers,
|
|
46
|
+
retrieval=self.retrieval,
|
|
47
|
+
compression=compression,
|
|
48
|
+
system_message=system_message,
|
|
49
|
+
guidelines=self.guidelines,
|
|
50
|
+
persistence=persistence,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def _runtime(
|
|
54
|
+
self,
|
|
55
|
+
*,
|
|
56
|
+
thread_id: str,
|
|
57
|
+
context: TContext,
|
|
58
|
+
run_id: str | None,
|
|
59
|
+
metadata: Mapping[str, Any] | None,
|
|
60
|
+
) -> ChatRuntime[TContext]:
|
|
61
|
+
return ChatRuntime(
|
|
62
|
+
thread_id=thread_id,
|
|
63
|
+
context=context,
|
|
64
|
+
run_id=run_id or str(uuid4()),
|
|
65
|
+
metadata=metadata or {},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def ainvoke(
|
|
69
|
+
self,
|
|
70
|
+
input: str,
|
|
71
|
+
*,
|
|
72
|
+
thread_id: str,
|
|
73
|
+
context: TContext,
|
|
74
|
+
run_id: str | None = None,
|
|
75
|
+
metadata: Mapping[str, Any] | None = None,
|
|
76
|
+
config: RunnableConfig | None = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
runtime = self._runtime(
|
|
79
|
+
thread_id=thread_id,
|
|
80
|
+
context=context,
|
|
81
|
+
run_id=run_id,
|
|
82
|
+
metadata=metadata,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
graph_config = with_runtime_config(config, runtime)
|
|
86
|
+
|
|
87
|
+
return await self._graph.ainvoke(
|
|
88
|
+
{"messages": [HumanMessage(content=input)]}, config=graph_config
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
async def astream_events(
|
|
92
|
+
self,
|
|
93
|
+
input: str,
|
|
94
|
+
*,
|
|
95
|
+
thread_id: str,
|
|
96
|
+
context: TContext,
|
|
97
|
+
run_id: str | None = None,
|
|
98
|
+
metadata: Mapping[str, Any] | None = None,
|
|
99
|
+
config: RunnableConfig | None = None,
|
|
100
|
+
version: Literal["v1", "v2"] = "v2",
|
|
101
|
+
) -> AsyncIterator[Any]:
|
|
102
|
+
runtime = self._runtime(
|
|
103
|
+
thread_id=thread_id,
|
|
104
|
+
context=context,
|
|
105
|
+
run_id=run_id,
|
|
106
|
+
metadata=metadata,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
graph_config = with_runtime_config(config, runtime)
|
|
110
|
+
|
|
111
|
+
async for event in self._graph.astream_events(
|
|
112
|
+
{"messages": [HumanMessage(content=input)]},
|
|
113
|
+
config=graph_config,
|
|
114
|
+
version=version,
|
|
115
|
+
):
|
|
116
|
+
yield event
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from .config import AutoCompress
|
|
2
|
+
from .strategies import KeepLatestN, SummarizeAll, SummarizeLatestN
|
|
3
|
+
from .types import CompressionStrategy
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"AutoCompress",
|
|
7
|
+
"CompressionStrategy",
|
|
8
|
+
"KeepLatestN",
|
|
9
|
+
"SummarizeAll",
|
|
10
|
+
"SummarizeLatestN",
|
|
11
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Generic, TypeVar, cast
|
|
4
|
+
|
|
5
|
+
from langchain.chat_models import BaseChatModel
|
|
6
|
+
from langchain_core.messages import BaseMessage
|
|
7
|
+
|
|
8
|
+
from autochat.config import ChatConfig
|
|
9
|
+
from autochat.runtime import ChatRuntime
|
|
10
|
+
|
|
11
|
+
from .strategies import SummarizeAll
|
|
12
|
+
from .types import CompressionStrategy
|
|
13
|
+
|
|
14
|
+
TContext = TypeVar("TContext")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _message_content_text(message: BaseMessage) -> str:
|
|
18
|
+
content = message.content
|
|
19
|
+
if isinstance(content, str):
|
|
20
|
+
return content
|
|
21
|
+
return str(content)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# TODO: Check if this approach is good
|
|
25
|
+
def _approximate_message_tokens(messages: list[BaseMessage]) -> int:
|
|
26
|
+
# Rough fallback used when the model cannot count tokens without extra deps.
|
|
27
|
+
return max(1, sum(len(_message_content_text(message)) for message in messages) // 4)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _count_message_tokens(
|
|
31
|
+
model: BaseChatModel,
|
|
32
|
+
messages: list[BaseMessage],
|
|
33
|
+
) -> int:
|
|
34
|
+
try:
|
|
35
|
+
with warnings.catch_warnings():
|
|
36
|
+
warnings.filterwarnings(
|
|
37
|
+
"ignore",
|
|
38
|
+
message="Using fallback GPT-2 tokenizer.*",
|
|
39
|
+
category=UserWarning,
|
|
40
|
+
)
|
|
41
|
+
return model.get_num_tokens_from_messages(messages)
|
|
42
|
+
except ImportError:
|
|
43
|
+
return _approximate_message_tokens(messages)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class AutoCompress(Generic[TContext]):
|
|
48
|
+
"""Automatic chat-history compression policy."""
|
|
49
|
+
|
|
50
|
+
at: float = 0.6
|
|
51
|
+
strategy: CompressionStrategy[TContext] = field(
|
|
52
|
+
default_factory=lambda: cast(CompressionStrategy[TContext], SummarizeAll())
|
|
53
|
+
)
|
|
54
|
+
context_window: int | None = None
|
|
55
|
+
model: BaseChatModel | None = None
|
|
56
|
+
|
|
57
|
+
def __post_init__(self) -> None:
|
|
58
|
+
if not 0 < self.at <= 1:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"AutoCompress.at must be greater than 0 and less than or equal to 1."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def acompress_if_needed(
|
|
64
|
+
self,
|
|
65
|
+
messages: list[BaseMessage],
|
|
66
|
+
*,
|
|
67
|
+
runtime: ChatRuntime[TContext],
|
|
68
|
+
config: ChatConfig,
|
|
69
|
+
) -> list[BaseMessage] | None:
|
|
70
|
+
context_window = self.context_window or config.context_window
|
|
71
|
+
if context_window is None:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"AutoCompress requires ChatConfig.context_window or "
|
|
74
|
+
"AutoCompress.context_window."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
model = self.model or config.model
|
|
78
|
+
used_tokens = _count_message_tokens(model, messages)
|
|
79
|
+
if used_tokens < context_window * self.at:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
return await self.strategy.acompress(
|
|
83
|
+
messages,
|
|
84
|
+
runtime=runtime,
|
|
85
|
+
model=model,
|
|
86
|
+
)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from langchain.chat_models import BaseChatModel
|
|
6
|
+
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
|
|
7
|
+
from langgraph.graph.message import REMOVE_ALL_MESSAGES, RemoveMessage
|
|
8
|
+
|
|
9
|
+
from autochat.runtime import ChatRuntime
|
|
10
|
+
|
|
11
|
+
TContext = TypeVar("TContext")
|
|
12
|
+
|
|
13
|
+
_SUMMARY_METADATA = {
|
|
14
|
+
"autochat": {
|
|
15
|
+
"kind": "compression_summary",
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _summary_message(summary: str) -> SystemMessage:
|
|
21
|
+
return SystemMessage(
|
|
22
|
+
content=f"Previous conversation summary:\n{summary}",
|
|
23
|
+
additional_kwargs=_SUMMARY_METADATA,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _replace_messages(
|
|
28
|
+
messages: Sequence[BaseMessage],
|
|
29
|
+
) -> list[BaseMessage]:
|
|
30
|
+
return [
|
|
31
|
+
RemoveMessage(id=REMOVE_ALL_MESSAGES),
|
|
32
|
+
*messages,
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _replace_with_summary_between(
|
|
37
|
+
messages_before: Sequence[BaseMessage],
|
|
38
|
+
summary: str,
|
|
39
|
+
messages_after: Sequence[BaseMessage],
|
|
40
|
+
) -> list[BaseMessage]:
|
|
41
|
+
return _replace_messages(
|
|
42
|
+
[
|
|
43
|
+
*messages_before,
|
|
44
|
+
_summary_message(summary),
|
|
45
|
+
*messages_after,
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _replace_with_latest(
|
|
51
|
+
messages_to_keep: Sequence[BaseMessage],
|
|
52
|
+
) -> list[BaseMessage]:
|
|
53
|
+
return _replace_messages(messages_to_keep)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _split_preserved_tail(
|
|
57
|
+
messages: Sequence[BaseMessage],
|
|
58
|
+
preserve_latest: int,
|
|
59
|
+
) -> tuple[list[BaseMessage], list[BaseMessage]]:
|
|
60
|
+
message_list = list(messages)
|
|
61
|
+
if preserve_latest <= 0:
|
|
62
|
+
return message_list, []
|
|
63
|
+
return message_list[:-preserve_latest], message_list[-preserve_latest:]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _message_text(message: BaseMessage) -> str:
|
|
67
|
+
content = message.content
|
|
68
|
+
if isinstance(content, str):
|
|
69
|
+
return content
|
|
70
|
+
return str(content)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _format_messages_for_summary(messages: Sequence[BaseMessage]) -> str:
|
|
74
|
+
lines: list[str] = []
|
|
75
|
+
for message in messages:
|
|
76
|
+
role = message.type
|
|
77
|
+
name = getattr(message, "name", None)
|
|
78
|
+
label = f"{role}:{name}" if name else role
|
|
79
|
+
lines.append(f"{label}: {_message_text(message)}")
|
|
80
|
+
return "\n".join(lines)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _summarize_messages(
|
|
84
|
+
messages: Sequence[BaseMessage],
|
|
85
|
+
*,
|
|
86
|
+
model: BaseChatModel,
|
|
87
|
+
) -> str:
|
|
88
|
+
prompt = (
|
|
89
|
+
"Summarize this conversation for future continuation. "
|
|
90
|
+
"Preserve user goals, decisions, constraints, tool results, and unresolved tasks. "
|
|
91
|
+
"Do not add facts that are not present.\n\n"
|
|
92
|
+
f"Conversation:\n{_format_messages_for_summary(messages)}"
|
|
93
|
+
)
|
|
94
|
+
response = await model.ainvoke([HumanMessage(content=prompt)])
|
|
95
|
+
content = response.content
|
|
96
|
+
if isinstance(content, str):
|
|
97
|
+
return content
|
|
98
|
+
return str(content)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, slots=True)
|
|
102
|
+
class KeepLatestN(Generic[TContext]):
|
|
103
|
+
"""Drop older history and keep only the latest N messages."""
|
|
104
|
+
|
|
105
|
+
n: int
|
|
106
|
+
|
|
107
|
+
def __post_init__(self) -> None:
|
|
108
|
+
if self.n < 1:
|
|
109
|
+
raise ValueError("KeepLatestN.n must be at least 1.")
|
|
110
|
+
|
|
111
|
+
async def acompress(
|
|
112
|
+
self,
|
|
113
|
+
messages: Sequence[BaseMessage],
|
|
114
|
+
*,
|
|
115
|
+
runtime: ChatRuntime[TContext],
|
|
116
|
+
model: BaseChatModel,
|
|
117
|
+
) -> list[BaseMessage]:
|
|
118
|
+
del runtime, model
|
|
119
|
+
return _replace_with_latest(list(messages)[-self.n :])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(frozen=True, slots=True)
|
|
123
|
+
class SummarizeAll(Generic[TContext]):
|
|
124
|
+
"""Summarize history into one summary and preserve the active latest messages."""
|
|
125
|
+
|
|
126
|
+
preserve_latest: int = 1
|
|
127
|
+
|
|
128
|
+
def __post_init__(self) -> None:
|
|
129
|
+
if self.preserve_latest < 0:
|
|
130
|
+
raise ValueError("SummarizeAll.preserve_latest cannot be negative.")
|
|
131
|
+
|
|
132
|
+
async def acompress(
|
|
133
|
+
self,
|
|
134
|
+
messages: Sequence[BaseMessage],
|
|
135
|
+
*,
|
|
136
|
+
runtime: ChatRuntime[TContext],
|
|
137
|
+
model: BaseChatModel,
|
|
138
|
+
) -> list[BaseMessage]:
|
|
139
|
+
del runtime
|
|
140
|
+
messages_to_summarize, messages_to_keep = _split_preserved_tail(
|
|
141
|
+
messages,
|
|
142
|
+
self.preserve_latest,
|
|
143
|
+
)
|
|
144
|
+
if not messages_to_summarize:
|
|
145
|
+
return _replace_with_latest(messages_to_keep)
|
|
146
|
+
|
|
147
|
+
summary = await _summarize_messages(messages_to_summarize, model=model)
|
|
148
|
+
return _replace_with_summary_between([], summary, messages_to_keep)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class SummarizeLatestN(Generic[TContext]):
|
|
153
|
+
"""Summarize the latest N history messages and preserve the active latest messages."""
|
|
154
|
+
|
|
155
|
+
n: int
|
|
156
|
+
preserve_latest: int = 1
|
|
157
|
+
|
|
158
|
+
def __post_init__(self) -> None:
|
|
159
|
+
if self.n < 1:
|
|
160
|
+
raise ValueError("SummarizeLatestN.n must be at least 1.")
|
|
161
|
+
if self.preserve_latest < 0:
|
|
162
|
+
raise ValueError("SummarizeLatestN.preserve_latest cannot be negative.")
|
|
163
|
+
|
|
164
|
+
async def acompress(
|
|
165
|
+
self,
|
|
166
|
+
messages: Sequence[BaseMessage],
|
|
167
|
+
*,
|
|
168
|
+
runtime: ChatRuntime[TContext],
|
|
169
|
+
model: BaseChatModel,
|
|
170
|
+
) -> list[BaseMessage]:
|
|
171
|
+
del runtime
|
|
172
|
+
history_messages, preserved_messages = _split_preserved_tail(
|
|
173
|
+
messages,
|
|
174
|
+
self.preserve_latest,
|
|
175
|
+
)
|
|
176
|
+
messages_to_summarize = history_messages[-self.n :]
|
|
177
|
+
messages_to_keep = history_messages[: -self.n]
|
|
178
|
+
if not messages_to_summarize:
|
|
179
|
+
return _replace_with_latest(preserved_messages)
|
|
180
|
+
|
|
181
|
+
summary = await _summarize_messages(messages_to_summarize, model=model)
|
|
182
|
+
return _replace_with_summary_between(
|
|
183
|
+
messages_to_keep,
|
|
184
|
+
summary,
|
|
185
|
+
preserved_messages,
|
|
186
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Generic, Protocol, TypeVar
|
|
3
|
+
|
|
4
|
+
from langchain.chat_models import BaseChatModel
|
|
5
|
+
from langchain_core.messages import BaseMessage
|
|
6
|
+
|
|
7
|
+
from autochat.runtime import ChatRuntime
|
|
8
|
+
|
|
9
|
+
TContext = TypeVar("TContext")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CompressionStrategy(Protocol[TContext]):
|
|
13
|
+
"""Strategy used by AutoCompress to compact chat history."""
|
|
14
|
+
|
|
15
|
+
async def acompress(
|
|
16
|
+
self,
|
|
17
|
+
messages: Sequence[BaseMessage],
|
|
18
|
+
*,
|
|
19
|
+
runtime: ChatRuntime[TContext],
|
|
20
|
+
model: BaseChatModel,
|
|
21
|
+
) -> list[BaseMessage]: ...
|
autochat/config.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from langchain.chat_models import BaseChatModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class ChatConfig:
|
|
9
|
+
model: BaseChatModel
|
|
10
|
+
model_kwargs: dict[str, Any] | None = None
|
|
11
|
+
context_window: int | None = None
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from langchain_core.messages import AIMessage, BaseMessage, SystemMessage
|
|
5
|
+
from langchain_core.runnables import RunnableConfig
|
|
6
|
+
from langgraph.checkpoint.base import BaseCheckpointSaver
|
|
7
|
+
from langgraph.graph import END, START, StateGraph
|
|
8
|
+
from langgraph.graph.state import CompiledStateGraph
|
|
9
|
+
|
|
10
|
+
from autochat.compression import AutoCompress
|
|
11
|
+
from autochat.config import ChatConfig
|
|
12
|
+
from autochat.graph.state import ChatGraphState, ChatGraphUpdate
|
|
13
|
+
from autochat.graph.tools import run_tool_calls
|
|
14
|
+
from autochat.graph.runtime import get_runtime
|
|
15
|
+
from autochat.guidelines import ChatGuideline
|
|
16
|
+
from autochat.retrieval import (
|
|
17
|
+
ChatRetriever,
|
|
18
|
+
RetrievalConfig,
|
|
19
|
+
)
|
|
20
|
+
from autochat.tools import ChatTool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_callable_names(
|
|
24
|
+
tools: Sequence[ChatTool[Any, Any, Any]],
|
|
25
|
+
retrievers: Sequence[ChatRetriever[Any]],
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Validate that tool and retriever names are unique."""
|
|
28
|
+
names = [tool.name for tool in tools] + [retriever.name for retriever in retrievers]
|
|
29
|
+
duplicates = {name for name in names if names.count(name) > 1}
|
|
30
|
+
|
|
31
|
+
if duplicates:
|
|
32
|
+
duplicate_names = ", ".join(sorted(duplicates))
|
|
33
|
+
raise ValueError(f"Duplicate tool/retriever names: {duplicate_names}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_system_messages(
|
|
37
|
+
system_message: str | None,
|
|
38
|
+
guidelines: Sequence[ChatGuideline],
|
|
39
|
+
) -> list[SystemMessage]:
|
|
40
|
+
messages: list[SystemMessage] = []
|
|
41
|
+
|
|
42
|
+
if system_message:
|
|
43
|
+
messages.append(SystemMessage(content=system_message))
|
|
44
|
+
|
|
45
|
+
if guidelines:
|
|
46
|
+
guideline_text = "\n".join(f"- {guideline.content}" for guideline in guidelines)
|
|
47
|
+
messages.append(SystemMessage(content=f"Guidelines:\n{guideline_text}"))
|
|
48
|
+
|
|
49
|
+
return messages
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def should_continue(state: ChatGraphState) -> str:
|
|
53
|
+
last_message = state["messages"][-1]
|
|
54
|
+
|
|
55
|
+
if isinstance(last_message, AIMessage) and last_message.tool_calls:
|
|
56
|
+
return "tools"
|
|
57
|
+
|
|
58
|
+
return END
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def build_chat_graph(
|
|
62
|
+
*,
|
|
63
|
+
config: ChatConfig,
|
|
64
|
+
tools: Sequence[ChatTool[Any, Any, Any]],
|
|
65
|
+
retrievers: Sequence[ChatRetriever[Any]],
|
|
66
|
+
retrieval: RetrievalConfig[Any],
|
|
67
|
+
compression: AutoCompress[Any] | None,
|
|
68
|
+
system_message: str | None,
|
|
69
|
+
guidelines: Sequence[ChatGuideline],
|
|
70
|
+
persistence: BaseCheckpointSaver | None,
|
|
71
|
+
) -> CompiledStateGraph:
|
|
72
|
+
graph = StateGraph(ChatGraphState)
|
|
73
|
+
system_messages = build_system_messages(system_message, guidelines)
|
|
74
|
+
|
|
75
|
+
chat_config = config
|
|
76
|
+
model = chat_config.model
|
|
77
|
+
if tools or retrievers:
|
|
78
|
+
validate_callable_names(tools, retrievers)
|
|
79
|
+
|
|
80
|
+
model = model.bind_tools(
|
|
81
|
+
[
|
|
82
|
+
*(tool.model_tool() for tool in tools),
|
|
83
|
+
*(retriever.model_tool() for retriever in retrievers),
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def call_model(
|
|
88
|
+
state: ChatGraphState,
|
|
89
|
+
config: RunnableConfig,
|
|
90
|
+
) -> ChatGraphUpdate:
|
|
91
|
+
messages: list[BaseMessage] = [
|
|
92
|
+
*system_messages,
|
|
93
|
+
*state["messages"],
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
response = await model.ainvoke(
|
|
97
|
+
messages, config=config, **(chat_config.model_kwargs or {})
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
response_messages: list[BaseMessage] = [response]
|
|
101
|
+
return {"messages": response_messages}
|
|
102
|
+
|
|
103
|
+
async def call_tools(
|
|
104
|
+
state: ChatGraphState,
|
|
105
|
+
config: RunnableConfig,
|
|
106
|
+
) -> ChatGraphUpdate:
|
|
107
|
+
last_message = state["messages"][-1]
|
|
108
|
+
|
|
109
|
+
if not isinstance(last_message, AIMessage):
|
|
110
|
+
return {"messages": []}
|
|
111
|
+
|
|
112
|
+
tool_messages = await run_tool_calls(
|
|
113
|
+
last_message,
|
|
114
|
+
tools,
|
|
115
|
+
retrievers,
|
|
116
|
+
config,
|
|
117
|
+
)
|
|
118
|
+
messages: list[BaseMessage] = list(tool_messages)
|
|
119
|
+
return {"messages": messages}
|
|
120
|
+
|
|
121
|
+
async def compress_messages(
|
|
122
|
+
state: ChatGraphState,
|
|
123
|
+
config: RunnableConfig,
|
|
124
|
+
) -> ChatGraphUpdate:
|
|
125
|
+
if compression is None:
|
|
126
|
+
return {"messages": []}
|
|
127
|
+
|
|
128
|
+
messages = await compression.acompress_if_needed(
|
|
129
|
+
list(state["messages"]),
|
|
130
|
+
runtime=get_runtime(config),
|
|
131
|
+
config=chat_config,
|
|
132
|
+
)
|
|
133
|
+
if messages is None:
|
|
134
|
+
return {"messages": []}
|
|
135
|
+
|
|
136
|
+
return {"messages": messages}
|
|
137
|
+
|
|
138
|
+
graph.add_node("agent", call_model)
|
|
139
|
+
graph.add_node("tools", call_tools)
|
|
140
|
+
|
|
141
|
+
if compression:
|
|
142
|
+
graph.add_node("compression", compress_messages)
|
|
143
|
+
graph.add_edge(START, "compression")
|
|
144
|
+
graph.add_edge("compression", "agent")
|
|
145
|
+
else:
|
|
146
|
+
graph.add_edge(START, "agent")
|
|
147
|
+
|
|
148
|
+
graph.add_conditional_edges("agent", should_continue)
|
|
149
|
+
graph.add_edge("tools", "agent")
|
|
150
|
+
|
|
151
|
+
return graph.compile(checkpointer=persistence)
|