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.
Files changed (43) hide show
  1. bbi_bucky-1.0.0/PKG-INFO +180 -0
  2. bbi_bucky-1.0.0/README.md +138 -0
  3. bbi_bucky-1.0.0/bbi_bucky/__init__.py +49 -0
  4. bbi_bucky-1.0.0/bbi_bucky/agent/__init__.py +2 -0
  5. bbi_bucky-1.0.0/bbi_bucky/agent/context.py +30 -0
  6. bbi_bucky-1.0.0/bbi_bucky/agent/nodes.py +103 -0
  7. bbi_bucky-1.0.0/bbi_bucky/agent/pipeline.py +193 -0
  8. bbi_bucky-1.0.0/bbi_bucky/agent/prompts.py +72 -0
  9. bbi_bucky-1.0.0/bbi_bucky/config/__init__.py +7 -0
  10. bbi_bucky-1.0.0/bbi_bucky/config/loader.py +115 -0
  11. bbi_bucky-1.0.0/bbi_bucky/config/schema.py +57 -0
  12. bbi_bucky-1.0.0/bbi_bucky/contracts/__init__.py +14 -0
  13. bbi_bucky-1.0.0/bbi_bucky/contracts/chat.py +24 -0
  14. bbi_bucky-1.0.0/bbi_bucky/contracts/envelope.py +145 -0
  15. bbi_bucky-1.0.0/bbi_bucky/contracts/sse_events.py +30 -0
  16. bbi_bucky-1.0.0/bbi_bucky/llm/__init__.py +13 -0
  17. bbi_bucky-1.0.0/bbi_bucky/llm/factory.py +67 -0
  18. bbi_bucky-1.0.0/bbi_bucky/llm/protocols.py +86 -0
  19. bbi_bucky-1.0.0/bbi_bucky/llm/providers/__init__.py +0 -0
  20. bbi_bucky-1.0.0/bbi_bucky/llm/providers/bedrock.py +148 -0
  21. bbi_bucky-1.0.0/bbi_bucky/llm/providers/cortex.py +187 -0
  22. bbi_bucky-1.0.0/bbi_bucky/llm/unified.py +80 -0
  23. bbi_bucky-1.0.0/bbi_bucky/memory/__init__.py +2 -0
  24. bbi_bucky-1.0.0/bbi_bucky/memory/inmemory.py +36 -0
  25. bbi_bucky-1.0.0/bbi_bucky/memory/interface.py +35 -0
  26. bbi_bucky-1.0.0/bbi_bucky/rag/__init__.py +3 -0
  27. bbi_bucky-1.0.0/bbi_bucky/rag/chromadb_backend.py +125 -0
  28. bbi_bucky-1.0.0/bbi_bucky/rag/factory.py +20 -0
  29. bbi_bucky-1.0.0/bbi_bucky/rag/interface.py +24 -0
  30. bbi_bucky-1.0.0/bbi_bucky/rag/null.py +21 -0
  31. bbi_bucky-1.0.0/bbi_bucky/response/__init__.py +1 -0
  32. bbi_bucky-1.0.0/bbi_bucky/response/formatter.py +88 -0
  33. bbi_bucky-1.0.0/bbi_bucky/router/__init__.py +3 -0
  34. bbi_bucky-1.0.0/bbi_bucky/router/factory.py +179 -0
  35. bbi_bucky-1.0.0/bbi_bucky/streaming/__init__.py +1 -0
  36. bbi_bucky-1.0.0/bbi_bucky/streaming/sse.py +28 -0
  37. bbi_bucky-1.0.0/bbi_bucky.egg-info/PKG-INFO +180 -0
  38. bbi_bucky-1.0.0/bbi_bucky.egg-info/SOURCES.txt +41 -0
  39. bbi_bucky-1.0.0/bbi_bucky.egg-info/dependency_links.txt +1 -0
  40. bbi_bucky-1.0.0/bbi_bucky.egg-info/requires.txt +31 -0
  41. bbi_bucky-1.0.0/bbi_bucky.egg-info/top_level.txt +1 -0
  42. bbi_bucky-1.0.0/pyproject.toml +64 -0
  43. bbi_bucky-1.0.0/setup.cfg +4 -0
@@ -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,2 @@
1
+ from bbi_bucky.agent.pipeline import ChatPipeline
2
+ from bbi_bucky.agent.context import ChatContext, ChatResult
@@ -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
+ )