bbi-bucky 1.0.0__tar.gz
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.
- bbi_bucky-1.0.0/PKG-INFO +180 -0
- bbi_bucky-1.0.0/README.md +138 -0
- bbi_bucky-1.0.0/bbi_bucky/__init__.py +49 -0
- bbi_bucky-1.0.0/bbi_bucky/agent/__init__.py +2 -0
- bbi_bucky-1.0.0/bbi_bucky/agent/context.py +30 -0
- bbi_bucky-1.0.0/bbi_bucky/agent/nodes.py +103 -0
- bbi_bucky-1.0.0/bbi_bucky/agent/pipeline.py +193 -0
- bbi_bucky-1.0.0/bbi_bucky/agent/prompts.py +72 -0
- bbi_bucky-1.0.0/bbi_bucky/config/__init__.py +7 -0
- bbi_bucky-1.0.0/bbi_bucky/config/loader.py +115 -0
- bbi_bucky-1.0.0/bbi_bucky/config/schema.py +57 -0
- bbi_bucky-1.0.0/bbi_bucky/contracts/__init__.py +14 -0
- bbi_bucky-1.0.0/bbi_bucky/contracts/chat.py +24 -0
- bbi_bucky-1.0.0/bbi_bucky/contracts/envelope.py +145 -0
- bbi_bucky-1.0.0/bbi_bucky/contracts/sse_events.py +30 -0
- bbi_bucky-1.0.0/bbi_bucky/llm/__init__.py +13 -0
- bbi_bucky-1.0.0/bbi_bucky/llm/factory.py +67 -0
- bbi_bucky-1.0.0/bbi_bucky/llm/protocols.py +86 -0
- bbi_bucky-1.0.0/bbi_bucky/llm/providers/__init__.py +0 -0
- bbi_bucky-1.0.0/bbi_bucky/llm/providers/bedrock.py +148 -0
- bbi_bucky-1.0.0/bbi_bucky/llm/providers/cortex.py +187 -0
- bbi_bucky-1.0.0/bbi_bucky/llm/unified.py +80 -0
- bbi_bucky-1.0.0/bbi_bucky/memory/__init__.py +2 -0
- bbi_bucky-1.0.0/bbi_bucky/memory/inmemory.py +36 -0
- bbi_bucky-1.0.0/bbi_bucky/memory/interface.py +35 -0
- bbi_bucky-1.0.0/bbi_bucky/rag/__init__.py +3 -0
- bbi_bucky-1.0.0/bbi_bucky/rag/chromadb_backend.py +125 -0
- bbi_bucky-1.0.0/bbi_bucky/rag/factory.py +20 -0
- bbi_bucky-1.0.0/bbi_bucky/rag/interface.py +24 -0
- bbi_bucky-1.0.0/bbi_bucky/rag/null.py +21 -0
- bbi_bucky-1.0.0/bbi_bucky/response/__init__.py +1 -0
- bbi_bucky-1.0.0/bbi_bucky/response/formatter.py +88 -0
- bbi_bucky-1.0.0/bbi_bucky/router/__init__.py +3 -0
- bbi_bucky-1.0.0/bbi_bucky/router/factory.py +179 -0
- bbi_bucky-1.0.0/bbi_bucky/streaming/__init__.py +1 -0
- bbi_bucky-1.0.0/bbi_bucky/streaming/sse.py +28 -0
- bbi_bucky-1.0.0/bbi_bucky.egg-info/PKG-INFO +180 -0
- bbi_bucky-1.0.0/bbi_bucky.egg-info/SOURCES.txt +41 -0
- bbi_bucky-1.0.0/bbi_bucky.egg-info/dependency_links.txt +1 -0
- bbi_bucky-1.0.0/bbi_bucky.egg-info/requires.txt +31 -0
- bbi_bucky-1.0.0/bbi_bucky.egg-info/top_level.txt +1 -0
- bbi_bucky-1.0.0/pyproject.toml +64 -0
- bbi_bucky-1.0.0/setup.cfg +4 -0
bbi_bucky-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bbi-bucky
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: BBI Bucky — pluggable AI chatbot framework for enterprise applications
|
|
5
|
+
Author-email: BBI Engineering <engineering@bbi.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bbi-engineering/bbi-bucky-framework
|
|
8
|
+
Project-URL: Documentation, https://github.com/bbi-engineering/bbi-bucky-framework#readme
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Framework :: FastAPI
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pydantic>=2.0
|
|
20
|
+
Provides-Extra: bedrock
|
|
21
|
+
Requires-Dist: boto3>=1.28.0; extra == "bedrock"
|
|
22
|
+
Provides-Extra: cortex
|
|
23
|
+
Requires-Dist: aiohttp>=3.8.0; extra == "cortex"
|
|
24
|
+
Provides-Extra: yaml
|
|
25
|
+
Requires-Dist: pyyaml>=6.0; extra == "yaml"
|
|
26
|
+
Provides-Extra: ruamel
|
|
27
|
+
Requires-Dist: ruamel.yaml>=0.17.0; extra == "ruamel"
|
|
28
|
+
Provides-Extra: api
|
|
29
|
+
Requires-Dist: fastapi>=0.100.0; extra == "api"
|
|
30
|
+
Requires-Dist: uvicorn>=0.23.0; extra == "api"
|
|
31
|
+
Provides-Extra: chromadb
|
|
32
|
+
Requires-Dist: chromadb>=0.4.0; extra == "chromadb"
|
|
33
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "chromadb"
|
|
34
|
+
Provides-Extra: all
|
|
35
|
+
Requires-Dist: bbi-bucky[api,bedrock,chromadb,cortex,ruamel,yaml]; extra == "all"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
39
|
+
Requires-Dist: httpx>=0.24; extra == "dev"
|
|
40
|
+
Requires-Dist: build; extra == "dev"
|
|
41
|
+
Requires-Dist: twine; extra == "dev"
|
|
42
|
+
|
|
43
|
+
# bbi-bucky
|
|
44
|
+
|
|
45
|
+
Pluggable AI chatbot framework for enterprise applications. Drop a YAML config and three lines of Python to add an AI assistant to any FastAPI app.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Core only (config, contracts, agent pipeline)
|
|
51
|
+
pip install bbi-bucky
|
|
52
|
+
|
|
53
|
+
# With AWS Bedrock (Claude)
|
|
54
|
+
pip install bbi-bucky[bedrock]
|
|
55
|
+
|
|
56
|
+
# With Snowflake Cortex
|
|
57
|
+
pip install bbi-bucky[cortex]
|
|
58
|
+
|
|
59
|
+
# With FastAPI router factory
|
|
60
|
+
pip install bbi-bucky[api]
|
|
61
|
+
|
|
62
|
+
# With RAG (ChromaDB)
|
|
63
|
+
pip install bbi-bucky[chromadb]
|
|
64
|
+
|
|
65
|
+
# YAML config loading
|
|
66
|
+
pip install bbi-bucky[yaml] # PyYAML
|
|
67
|
+
pip install bbi-bucky[ruamel] # ruamel.yaml
|
|
68
|
+
|
|
69
|
+
# Everything
|
|
70
|
+
pip install bbi-bucky[all]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
**1. Create a config file** (`bucky.yaml`):
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
app_name: "My App Assistant"
|
|
79
|
+
llm:
|
|
80
|
+
provider: "bedrock"
|
|
81
|
+
model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
|
82
|
+
temperature: 0.7
|
|
83
|
+
max_tokens: 4096
|
|
84
|
+
aws_region: "us-east-1"
|
|
85
|
+
agent:
|
|
86
|
+
max_history: 10
|
|
87
|
+
system_prompt_override: |
|
|
88
|
+
You are a helpful assistant for My App.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**2. Wire it up** (3 lines):
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from bbi_bucky import create_chat_router, load_config
|
|
95
|
+
|
|
96
|
+
config = load_config(yaml_path="bucky.yaml")
|
|
97
|
+
app.include_router(create_chat_router(config=config))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This gives you `/api/bucky/chat`, `/api/bucky/chat/stream` (SSE), and `/api/bucky/health` out of the box.
|
|
101
|
+
|
|
102
|
+
**3. Or use the LLM service directly:**
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from bbi_bucky import load_config
|
|
106
|
+
from bbi_bucky.llm.unified import UnifiedLLMService
|
|
107
|
+
|
|
108
|
+
config = load_config(yaml_path="bucky.yaml")
|
|
109
|
+
llm = UnifiedLLMService(config.llm)
|
|
110
|
+
|
|
111
|
+
# Non-streaming
|
|
112
|
+
response = await llm.invoke("What is Kubernetes?", system_prompt="Be concise.")
|
|
113
|
+
|
|
114
|
+
# Streaming
|
|
115
|
+
async for chunk in llm.stream("Explain microservices"):
|
|
116
|
+
print(chunk, end="")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
All config is loaded from YAML, with environment variable overrides and code overrides layered on top.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
config = load_config(
|
|
125
|
+
yaml_path="bucky.yaml", # YAML file (optional)
|
|
126
|
+
env_prefix="BBI_BUCKY_", # Env var prefix (e.g. BBI_BUCKY_LLM_PROVIDER=cortex)
|
|
127
|
+
overrides={"llm": {"model": "llama3.1-70b"}}, # Code overrides (highest priority)
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Priority: YAML < environment variables < code overrides.
|
|
132
|
+
|
|
133
|
+
## LLM Providers
|
|
134
|
+
|
|
135
|
+
### AWS Bedrock (Claude)
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pip install bbi-bucky[bedrock]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Uses the Bedrock Converse API. Supports streaming. Credentials from environment or explicit config.
|
|
142
|
+
|
|
143
|
+
### Snowflake Cortex
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pip install bbi-bucky[cortex]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
REST API with SSE streaming. SPCS-compatible (auto-detects `/snowflake/session/token`).
|
|
150
|
+
|
|
151
|
+
### Custom providers
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from bbi_bucky.llm.factory import LLMFactory
|
|
155
|
+
|
|
156
|
+
LLMFactory.register("my_provider", MyCustomProvider)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Your provider just needs `generate()` and `stream()` methods matching the `LLMProvider` protocol.
|
|
160
|
+
|
|
161
|
+
## Features
|
|
162
|
+
|
|
163
|
+
- **Zero-config defaults**: Only `pydantic` is required. Everything else is optional.
|
|
164
|
+
- **YAML + env var config**: Single source of truth, overridable per-environment.
|
|
165
|
+
- **Multiple LLM providers**: Bedrock and Cortex built-in, custom providers via `register()`.
|
|
166
|
+
- **SSE streaming**: Named events (`response.text.delta`, `activity.started`, etc.).
|
|
167
|
+
- **Agent pipeline**: Intent classification, context gathering, RAG retrieval, response generation.
|
|
168
|
+
- **RAG support**: ChromaDB backend included, or bring your own via `IRAGService`.
|
|
169
|
+
- **Conversation memory**: In-memory by default, extensible via `IConversationMemory`.
|
|
170
|
+
- **API contracts**: `ApiResponseEnvelope` on all endpoints (RFC 9457 errors).
|
|
171
|
+
- **Router factory**: `create_chat_router()` gives you a full FastAPI router in one call.
|
|
172
|
+
|
|
173
|
+
## Requirements
|
|
174
|
+
|
|
175
|
+
- Python 3.11+
|
|
176
|
+
- `pydantic >= 2.0` (only hard dependency)
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# bbi-bucky
|
|
2
|
+
|
|
3
|
+
Pluggable AI chatbot framework for enterprise applications. Drop a YAML config and three lines of Python to add an AI assistant to any FastAPI app.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Core only (config, contracts, agent pipeline)
|
|
9
|
+
pip install bbi-bucky
|
|
10
|
+
|
|
11
|
+
# With AWS Bedrock (Claude)
|
|
12
|
+
pip install bbi-bucky[bedrock]
|
|
13
|
+
|
|
14
|
+
# With Snowflake Cortex
|
|
15
|
+
pip install bbi-bucky[cortex]
|
|
16
|
+
|
|
17
|
+
# With FastAPI router factory
|
|
18
|
+
pip install bbi-bucky[api]
|
|
19
|
+
|
|
20
|
+
# With RAG (ChromaDB)
|
|
21
|
+
pip install bbi-bucky[chromadb]
|
|
22
|
+
|
|
23
|
+
# YAML config loading
|
|
24
|
+
pip install bbi-bucky[yaml] # PyYAML
|
|
25
|
+
pip install bbi-bucky[ruamel] # ruamel.yaml
|
|
26
|
+
|
|
27
|
+
# Everything
|
|
28
|
+
pip install bbi-bucky[all]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick start
|
|
32
|
+
|
|
33
|
+
**1. Create a config file** (`bucky.yaml`):
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
app_name: "My App Assistant"
|
|
37
|
+
llm:
|
|
38
|
+
provider: "bedrock"
|
|
39
|
+
model: "us.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
|
40
|
+
temperature: 0.7
|
|
41
|
+
max_tokens: 4096
|
|
42
|
+
aws_region: "us-east-1"
|
|
43
|
+
agent:
|
|
44
|
+
max_history: 10
|
|
45
|
+
system_prompt_override: |
|
|
46
|
+
You are a helpful assistant for My App.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**2. Wire it up** (3 lines):
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from bbi_bucky import create_chat_router, load_config
|
|
53
|
+
|
|
54
|
+
config = load_config(yaml_path="bucky.yaml")
|
|
55
|
+
app.include_router(create_chat_router(config=config))
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This gives you `/api/bucky/chat`, `/api/bucky/chat/stream` (SSE), and `/api/bucky/health` out of the box.
|
|
59
|
+
|
|
60
|
+
**3. Or use the LLM service directly:**
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from bbi_bucky import load_config
|
|
64
|
+
from bbi_bucky.llm.unified import UnifiedLLMService
|
|
65
|
+
|
|
66
|
+
config = load_config(yaml_path="bucky.yaml")
|
|
67
|
+
llm = UnifiedLLMService(config.llm)
|
|
68
|
+
|
|
69
|
+
# Non-streaming
|
|
70
|
+
response = await llm.invoke("What is Kubernetes?", system_prompt="Be concise.")
|
|
71
|
+
|
|
72
|
+
# Streaming
|
|
73
|
+
async for chunk in llm.stream("Explain microservices"):
|
|
74
|
+
print(chunk, end="")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
All config is loaded from YAML, with environment variable overrides and code overrides layered on top.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
config = load_config(
|
|
83
|
+
yaml_path="bucky.yaml", # YAML file (optional)
|
|
84
|
+
env_prefix="BBI_BUCKY_", # Env var prefix (e.g. BBI_BUCKY_LLM_PROVIDER=cortex)
|
|
85
|
+
overrides={"llm": {"model": "llama3.1-70b"}}, # Code overrides (highest priority)
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Priority: YAML < environment variables < code overrides.
|
|
90
|
+
|
|
91
|
+
## LLM Providers
|
|
92
|
+
|
|
93
|
+
### AWS Bedrock (Claude)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install bbi-bucky[bedrock]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Uses the Bedrock Converse API. Supports streaming. Credentials from environment or explicit config.
|
|
100
|
+
|
|
101
|
+
### Snowflake Cortex
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install bbi-bucky[cortex]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
REST API with SSE streaming. SPCS-compatible (auto-detects `/snowflake/session/token`).
|
|
108
|
+
|
|
109
|
+
### Custom providers
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from bbi_bucky.llm.factory import LLMFactory
|
|
113
|
+
|
|
114
|
+
LLMFactory.register("my_provider", MyCustomProvider)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Your provider just needs `generate()` and `stream()` methods matching the `LLMProvider` protocol.
|
|
118
|
+
|
|
119
|
+
## Features
|
|
120
|
+
|
|
121
|
+
- **Zero-config defaults**: Only `pydantic` is required. Everything else is optional.
|
|
122
|
+
- **YAML + env var config**: Single source of truth, overridable per-environment.
|
|
123
|
+
- **Multiple LLM providers**: Bedrock and Cortex built-in, custom providers via `register()`.
|
|
124
|
+
- **SSE streaming**: Named events (`response.text.delta`, `activity.started`, etc.).
|
|
125
|
+
- **Agent pipeline**: Intent classification, context gathering, RAG retrieval, response generation.
|
|
126
|
+
- **RAG support**: ChromaDB backend included, or bring your own via `IRAGService`.
|
|
127
|
+
- **Conversation memory**: In-memory by default, extensible via `IConversationMemory`.
|
|
128
|
+
- **API contracts**: `ApiResponseEnvelope` on all endpoints (RFC 9457 errors).
|
|
129
|
+
- **Router factory**: `create_chat_router()` gives you a full FastAPI router in one call.
|
|
130
|
+
|
|
131
|
+
## Requirements
|
|
132
|
+
|
|
133
|
+
- Python 3.11+
|
|
134
|
+
- `pydantic >= 2.0` (only hard dependency)
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""BBI Bucky — AI chatbot framework for BBI applications."""
|
|
2
|
+
|
|
3
|
+
from bbi_bucky.config.loader import load_config
|
|
4
|
+
from bbi_bucky.config.schema import BbiBuckyConfig, LLMProviderConfig, RAGConfig, AgentConfig
|
|
5
|
+
from bbi_bucky.contracts.envelope import ApiResponseEnvelope, build_response_envelope
|
|
6
|
+
from bbi_bucky.agent.context import ChatContext, ChatResult
|
|
7
|
+
|
|
8
|
+
__version__ = "1.0.0"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_chat_router(**kwargs):
|
|
12
|
+
"""Lazy import to avoid requiring fastapi at module load time."""
|
|
13
|
+
from bbi_bucky.router.factory import create_chat_router as _create
|
|
14
|
+
|
|
15
|
+
return _create(**kwargs)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __getattr__(name: str):
|
|
19
|
+
if name == "UnifiedLLMService":
|
|
20
|
+
from bbi_bucky.llm.unified import UnifiedLLMService
|
|
21
|
+
return UnifiedLLMService
|
|
22
|
+
if name == "IRAGService":
|
|
23
|
+
from bbi_bucky.rag.interface import IRAGService
|
|
24
|
+
return IRAGService
|
|
25
|
+
if name == "create_rag_service":
|
|
26
|
+
from bbi_bucky.rag.factory import create_rag_service
|
|
27
|
+
return create_rag_service
|
|
28
|
+
if name == "ChatPipeline":
|
|
29
|
+
from bbi_bucky.agent.pipeline import ChatPipeline
|
|
30
|
+
return ChatPipeline
|
|
31
|
+
raise AttributeError(f"module 'bbi_bucky' has no attribute {name!r}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"create_chat_router",
|
|
36
|
+
"load_config",
|
|
37
|
+
"BbiBuckyConfig",
|
|
38
|
+
"LLMProviderConfig",
|
|
39
|
+
"RAGConfig",
|
|
40
|
+
"AgentConfig",
|
|
41
|
+
"UnifiedLLMService",
|
|
42
|
+
"IRAGService",
|
|
43
|
+
"create_rag_service",
|
|
44
|
+
"ApiResponseEnvelope",
|
|
45
|
+
"build_response_envelope",
|
|
46
|
+
"ChatPipeline",
|
|
47
|
+
"ChatContext",
|
|
48
|
+
"ChatResult",
|
|
49
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Agent context and result data models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ChatContext:
|
|
9
|
+
"""Context sent with every chat message."""
|
|
10
|
+
|
|
11
|
+
session_id: str | None = None
|
|
12
|
+
user_id: str | None = None
|
|
13
|
+
current_page: str | None = None
|
|
14
|
+
current_tool: str | None = None
|
|
15
|
+
page_context: dict[str, Any] = field(default_factory=dict)
|
|
16
|
+
conversation_history: list[dict[str, str]] = field(default_factory=list)
|
|
17
|
+
system_context: str | None = None
|
|
18
|
+
uploaded_files: list[str] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ChatResult:
|
|
23
|
+
"""Result of processing a chat message."""
|
|
24
|
+
|
|
25
|
+
response: str
|
|
26
|
+
suggestions: list[str] = field(default_factory=list)
|
|
27
|
+
sources: list[dict[str, Any]] = field(default_factory=list)
|
|
28
|
+
intent: str | None = None
|
|
29
|
+
confidence: float = 0.0
|
|
30
|
+
conversation_id: str | None = None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Agent pipeline node functions."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from bbi_bucky.agent.context import ChatContext
|
|
8
|
+
from bbi_bucky.agent.prompts import INTENT_DETECTION_PROMPT
|
|
9
|
+
from bbi_bucky.llm.unified import UnifiedLLMService
|
|
10
|
+
from bbi_bucky.rag.interface import IRAGService
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def classify_intent(
|
|
16
|
+
query: str,
|
|
17
|
+
context: ChatContext,
|
|
18
|
+
llm: UnifiedLLMService,
|
|
19
|
+
) -> dict[str, Any]:
|
|
20
|
+
"""Classify the user's intent using the LLM."""
|
|
21
|
+
history_str = ""
|
|
22
|
+
if context.conversation_history:
|
|
23
|
+
recent = context.conversation_history[-4:]
|
|
24
|
+
history_str = "\n".join(
|
|
25
|
+
f"{turn['role']}: {turn['content'][:200]}" for turn in recent
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
prompt = INTENT_DETECTION_PROMPT.format(query=query, history=history_str)
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
raw = await llm.invoke(prompt, system_prompt="You are an intent classifier. Respond only with valid JSON.")
|
|
32
|
+
result = json.loads(raw.strip().strip("```json").strip("```"))
|
|
33
|
+
return {
|
|
34
|
+
"intent": result.get("intent", "general"),
|
|
35
|
+
"confidence": float(result.get("confidence", 0.5)),
|
|
36
|
+
}
|
|
37
|
+
except Exception as e:
|
|
38
|
+
logger.warning(f"Intent classification failed: {e}")
|
|
39
|
+
return {"intent": "general", "confidence": 0.5}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def gather_context(
|
|
43
|
+
query: str,
|
|
44
|
+
context: ChatContext,
|
|
45
|
+
rag_results: dict[str, Any] | None = None,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Build the full context string for the LLM prompt."""
|
|
48
|
+
parts: list[str] = []
|
|
49
|
+
|
|
50
|
+
if context.current_page:
|
|
51
|
+
parts.append(f"Current page: {context.current_page}")
|
|
52
|
+
if context.current_tool:
|
|
53
|
+
parts.append(f"Current tool: {context.current_tool}")
|
|
54
|
+
|
|
55
|
+
if context.page_context:
|
|
56
|
+
page_ctx = json.dumps(context.page_context, default=str)
|
|
57
|
+
if len(page_ctx) > 2000:
|
|
58
|
+
page_ctx = page_ctx[:2000] + "..."
|
|
59
|
+
parts.append(f"Page context:\n{page_ctx}")
|
|
60
|
+
|
|
61
|
+
if context.system_context:
|
|
62
|
+
parts.append(f"System context:\n{context.system_context[:1000]}")
|
|
63
|
+
|
|
64
|
+
if context.conversation_history:
|
|
65
|
+
recent = context.conversation_history[-6:]
|
|
66
|
+
history_lines = [f"{t['role']}: {t['content'][:300]}" for t in recent]
|
|
67
|
+
parts.append(f"Conversation history:\n" + "\n".join(history_lines))
|
|
68
|
+
|
|
69
|
+
if rag_results and rag_results.get("results"):
|
|
70
|
+
rag_context = "\n\n".join(
|
|
71
|
+
f"[Source: {r.get('metadata', {}).get('source', 'unknown')}]\n{r['content'][:500]}"
|
|
72
|
+
for r in rag_results["results"][:3]
|
|
73
|
+
)
|
|
74
|
+
parts.append(f"Retrieved knowledge:\n{rag_context}")
|
|
75
|
+
|
|
76
|
+
return "\n\n---\n\n".join(parts) if parts else ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def generate_response(
|
|
80
|
+
query: str,
|
|
81
|
+
context_str: str,
|
|
82
|
+
intent: dict[str, Any],
|
|
83
|
+
llm: UnifiedLLMService,
|
|
84
|
+
system_prompt: str,
|
|
85
|
+
mode_prompt: str,
|
|
86
|
+
) -> str:
|
|
87
|
+
"""Generate the final response using the LLM."""
|
|
88
|
+
full_prompt_parts = [
|
|
89
|
+
f"User question: {query}",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
if context_str:
|
|
93
|
+
full_prompt_parts.append(f"\nAvailable context:\n{context_str}")
|
|
94
|
+
|
|
95
|
+
full_prompt_parts.append(
|
|
96
|
+
"\nProvide a helpful, well-formatted response. "
|
|
97
|
+
"Include 3 relevant follow-up questions at the end."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
full_system = f"{system_prompt}\n\n{mode_prompt}"
|
|
101
|
+
full_prompt = "\n".join(full_prompt_parts)
|
|
102
|
+
|
|
103
|
+
return await llm.invoke(full_prompt, system_prompt=full_system)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""Chat pipeline — linear agent that classifies intent, gathers context, retrieves from RAG, and generates a response."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any, AsyncGenerator
|
|
6
|
+
|
|
7
|
+
from bbi_bucky.agent.context import ChatContext, ChatResult
|
|
8
|
+
from bbi_bucky.agent.nodes import classify_intent, gather_context, generate_response
|
|
9
|
+
from bbi_bucky.agent.prompts import MODE_PROMPTS, SYSTEM_PROMPT
|
|
10
|
+
from bbi_bucky.config.schema import AgentConfig
|
|
11
|
+
from bbi_bucky.contracts.sse_events import SSEEvent, SSEEventType
|
|
12
|
+
from bbi_bucky.llm.unified import UnifiedLLMService
|
|
13
|
+
from bbi_bucky.memory.interface import IConversationMemory
|
|
14
|
+
from bbi_bucky.rag.interface import IRAGService
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChatPipeline:
|
|
20
|
+
"""Linear agent pipeline: intent → context → RAG → respond."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
llm: UnifiedLLMService,
|
|
25
|
+
rag: IRAGService | None = None,
|
|
26
|
+
memory: IConversationMemory | None = None,
|
|
27
|
+
config: AgentConfig | None = None,
|
|
28
|
+
):
|
|
29
|
+
self._llm = llm
|
|
30
|
+
self._rag = rag
|
|
31
|
+
self._memory = memory
|
|
32
|
+
self._config = config or AgentConfig()
|
|
33
|
+
|
|
34
|
+
async def process(
|
|
35
|
+
self,
|
|
36
|
+
query: str,
|
|
37
|
+
context: ChatContext,
|
|
38
|
+
mode: str | None = None,
|
|
39
|
+
) -> ChatResult:
|
|
40
|
+
mode = mode or self._config.default_mode
|
|
41
|
+
conversation_id = context.session_id or str(uuid.uuid4())
|
|
42
|
+
|
|
43
|
+
# Load history from memory if available
|
|
44
|
+
if self._memory and context.session_id:
|
|
45
|
+
history = self._memory.get_history(context.session_id, self._config.max_history)
|
|
46
|
+
for turn in history:
|
|
47
|
+
context.conversation_history.append({"role": "user", "content": turn.user_message})
|
|
48
|
+
context.conversation_history.append({"role": "assistant", "content": turn.assistant_response})
|
|
49
|
+
|
|
50
|
+
# 1. Classify intent
|
|
51
|
+
intent = {"intent": "general", "confidence": 0.5}
|
|
52
|
+
if self._config.enable_intent_detection:
|
|
53
|
+
intent = await classify_intent(query, context, self._llm)
|
|
54
|
+
|
|
55
|
+
# 2. RAG retrieval
|
|
56
|
+
rag_results: dict[str, Any] | None = None
|
|
57
|
+
if self._rag:
|
|
58
|
+
rag_results = await self._rag.retrieve_with_sources(query, k=5)
|
|
59
|
+
|
|
60
|
+
# 3. Gather context
|
|
61
|
+
context_str = gather_context(query, context, rag_results)
|
|
62
|
+
|
|
63
|
+
# 4. Generate response
|
|
64
|
+
system_prompt = self._config.system_prompt_override or SYSTEM_PROMPT
|
|
65
|
+
mode_prompt = MODE_PROMPTS.get(mode, MODE_PROMPTS["conversational"])
|
|
66
|
+
|
|
67
|
+
response_text = await generate_response(
|
|
68
|
+
query=query,
|
|
69
|
+
context_str=context_str,
|
|
70
|
+
intent=intent,
|
|
71
|
+
llm=self._llm,
|
|
72
|
+
system_prompt=system_prompt,
|
|
73
|
+
mode_prompt=mode_prompt,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Store in memory
|
|
77
|
+
if self._memory and context.session_id:
|
|
78
|
+
await self._memory.store(
|
|
79
|
+
session_id=context.session_id,
|
|
80
|
+
user_msg=query,
|
|
81
|
+
assistant_msg=response_text,
|
|
82
|
+
metadata={"intent": intent.get("intent"), "mode": mode},
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
sources = rag_results.get("sources", []) if rag_results else []
|
|
86
|
+
|
|
87
|
+
return ChatResult(
|
|
88
|
+
response=response_text,
|
|
89
|
+
suggestions=[],
|
|
90
|
+
sources=[{"source": s} for s in sources],
|
|
91
|
+
intent=intent.get("intent"),
|
|
92
|
+
confidence=intent.get("confidence", 0.0),
|
|
93
|
+
conversation_id=conversation_id,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def process_streaming(
|
|
97
|
+
self,
|
|
98
|
+
query: str,
|
|
99
|
+
context: ChatContext,
|
|
100
|
+
mode: str | None = None,
|
|
101
|
+
) -> AsyncGenerator[SSEEvent, None]:
|
|
102
|
+
mode = mode or self._config.default_mode
|
|
103
|
+
conversation_id = context.session_id or str(uuid.uuid4())
|
|
104
|
+
|
|
105
|
+
# Load history
|
|
106
|
+
if self._memory and context.session_id:
|
|
107
|
+
history = self._memory.get_history(context.session_id, self._config.max_history)
|
|
108
|
+
for turn in history:
|
|
109
|
+
context.conversation_history.append({"role": "user", "content": turn.user_message})
|
|
110
|
+
context.conversation_history.append({"role": "assistant", "content": turn.assistant_response})
|
|
111
|
+
|
|
112
|
+
yield SSEEvent(
|
|
113
|
+
event=SSEEventType.ACTIVITY_STARTED,
|
|
114
|
+
data={"step": "intent_detection", "label": "Understanding your question..."},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# 1. Classify intent
|
|
118
|
+
intent = {"intent": "general", "confidence": 0.5}
|
|
119
|
+
if self._config.enable_intent_detection:
|
|
120
|
+
intent = await classify_intent(query, context, self._llm)
|
|
121
|
+
|
|
122
|
+
yield SSEEvent(
|
|
123
|
+
event=SSEEventType.ACTIVITY_COMPLETED,
|
|
124
|
+
data={"step": "intent_detection", "intent": intent.get("intent")},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# 2. RAG retrieval
|
|
128
|
+
rag_results: dict[str, Any] | None = None
|
|
129
|
+
if self._rag:
|
|
130
|
+
yield SSEEvent(
|
|
131
|
+
event=SSEEventType.ACTIVITY_STARTED,
|
|
132
|
+
data={"step": "rag_retrieval", "label": "Searching knowledge base..."},
|
|
133
|
+
)
|
|
134
|
+
rag_results = await self._rag.retrieve_with_sources(query, k=5)
|
|
135
|
+
yield SSEEvent(
|
|
136
|
+
event=SSEEventType.ACTIVITY_COMPLETED,
|
|
137
|
+
data={
|
|
138
|
+
"step": "rag_retrieval",
|
|
139
|
+
"count": len(rag_results.get("results", [])),
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# 3. Gather context
|
|
144
|
+
context_str = gather_context(query, context, rag_results)
|
|
145
|
+
|
|
146
|
+
yield SSEEvent(
|
|
147
|
+
event=SSEEventType.CONTEXT_RESOLVED,
|
|
148
|
+
data={"context_length": len(context_str)},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# 4. Stream response
|
|
152
|
+
system_prompt = self._config.system_prompt_override or SYSTEM_PROMPT
|
|
153
|
+
mode_prompt = MODE_PROMPTS.get(mode, MODE_PROMPTS["conversational"])
|
|
154
|
+
full_system = f"{system_prompt}\n\n{mode_prompt}"
|
|
155
|
+
|
|
156
|
+
prompt_parts = [f"User question: {query}"]
|
|
157
|
+
if context_str:
|
|
158
|
+
prompt_parts.append(f"\nAvailable context:\n{context_str}")
|
|
159
|
+
prompt_parts.append(
|
|
160
|
+
"\nProvide a helpful, well-formatted response. "
|
|
161
|
+
"Include 3 relevant follow-up questions at the end."
|
|
162
|
+
)
|
|
163
|
+
full_prompt = "\n".join(prompt_parts)
|
|
164
|
+
|
|
165
|
+
full_response: list[str] = []
|
|
166
|
+
async for token in self._llm.stream(full_prompt, system_prompt=full_system):
|
|
167
|
+
full_response.append(token)
|
|
168
|
+
yield SSEEvent(
|
|
169
|
+
event=SSEEventType.RESPONSE_TEXT_DELTA,
|
|
170
|
+
data={"delta": token},
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
response_text = "".join(full_response)
|
|
174
|
+
|
|
175
|
+
# Store in memory
|
|
176
|
+
if self._memory and context.session_id:
|
|
177
|
+
await self._memory.store(
|
|
178
|
+
session_id=context.session_id,
|
|
179
|
+
user_msg=query,
|
|
180
|
+
assistant_msg=response_text,
|
|
181
|
+
metadata={"intent": intent.get("intent"), "mode": mode},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
sources = rag_results.get("sources", []) if rag_results else []
|
|
185
|
+
|
|
186
|
+
yield SSEEvent(
|
|
187
|
+
event=SSEEventType.RESPONSE_COMPLETED,
|
|
188
|
+
data={
|
|
189
|
+
"conversation_id": conversation_id,
|
|
190
|
+
"intent": intent.get("intent"),
|
|
191
|
+
"sources": sources,
|
|
192
|
+
},
|
|
193
|
+
)
|