feat: ms-bot-framework with dynamic adaptive cards
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
from typing import Dict, Any
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.prompts import ChatPromptTemplate
|
||||
from langchain.chains import LLMChain
|
||||
import asyncio
|
||||
|
||||
class AdaptiveCards:
|
||||
def __init__(self):
|
||||
self.llm = ChatOpenAI(temperature=0)
|
||||
self.prompt = ChatPromptTemplate.from_template("""
|
||||
You are a Microsoft Adaptive Card generator. Given a data schema and known values,
|
||||
generate an Adaptive Card (v1.3) that asks the user only for missing fields.
|
||||
|
||||
Use this schema: https://adaptivecards.io/schemas/adaptive-card.json
|
||||
Respond only with valid Adaptive Card JSON. Do not include explanations.
|
||||
Always include isRequired, and errorMessage in the schema.
|
||||
Always include a submit button at the bottom of the card as defined in the schema.
|
||||
|
||||
### Schema:
|
||||
{schema}
|
||||
|
||||
### Known values:
|
||||
{known_values}
|
||||
|
||||
""")
|
||||
self.chain = LLMChain(llm=self.llm, prompt=self.prompt)
|
||||
|
||||
async def generate_card(self, schema: Dict[str, Any], known_values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(None, self.chain.run, {
|
||||
"schema": schema,
|
||||
"known_values": known_values
|
||||
})
|
||||
|
||||
def create_welcome_card(self):
|
||||
"""Create a welcome card"""
|
||||
return {
|
||||
"type": "AdaptiveCard",
|
||||
"body": [
|
||||
{
|
||||
"type": "TextBlock",
|
||||
"text": "Welcome to the Housing Bot!",
|
||||
"size": "large"
|
||||
}
|
||||
],
|
||||
"version": "1.0"
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import json
|
||||
from typing import Annotated
|
||||
from fastapi import Depends
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from botbuilder.core import ActivityHandler, TurnContext
|
||||
from botbuilder.schema import Activity, Attachment, ActivityTypes
|
||||
import asyncio
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from backend.app.bots.adaptive_cards import AdaptiveCards
|
||||
from backend.app.bots.intent_detector import IntentDetector
|
||||
from backend.app.bots.slot_filler import SlotFiller
|
||||
from backend.app.dtos.house.house_features import HouseFeatures
|
||||
from backend.app.services.house_price_predictor import HousePricePredictor
|
||||
|
||||
class Dayta(ActivityHandler):
|
||||
def __init__(
|
||||
self,
|
||||
intent_detector: Annotated[IntentDetector, Depends()],
|
||||
card_bot: Annotated[AdaptiveCards, Depends()],
|
||||
slot_filler: Annotated[SlotFiller, Depends()],
|
||||
price_predictor: Annotated[HousePricePredictor, Depends()],):
|
||||
|
||||
self.intent_detector = intent_detector
|
||||
self.card_bot = card_bot
|
||||
self.slot_filler = slot_filler
|
||||
self.price_predictor = price_predictor
|
||||
self.chat_llm = ChatOpenAI(temperature=0.7)
|
||||
self.user_sessions = {}
|
||||
|
||||
async def on_message_activity(self, turn_context: TurnContext):
|
||||
user_message = turn_context.activity.text
|
||||
user_id = turn_context.activity.from_property.id
|
||||
submitted_values = turn_context.activity.value
|
||||
|
||||
known_values = self.user_sessions.get(user_id, {})
|
||||
schema = HouseFeatures.model_json_schema()
|
||||
#required_fields = list(HouseFeatures.model_fields.keys())
|
||||
required_fields = [
|
||||
name for name, field in HouseFeatures.model_fields.items()
|
||||
if field.is_required()
|
||||
]
|
||||
print(f"required_fields: {required_fields}")
|
||||
|
||||
# Update known values
|
||||
if submitted_values is not None:
|
||||
known_values.update(submitted_values)
|
||||
else:
|
||||
extracted = await self.slot_filler.extract_slots(schema, user_message)
|
||||
known_values.update(extracted)
|
||||
|
||||
self.user_sessions[user_id] = known_values
|
||||
|
||||
# Detect intent only if message-based
|
||||
if not submitted_values:
|
||||
intent = await self.intent_detector.detect_intent(user_message)
|
||||
if intent.strip().lower() in ("unknown", ""):
|
||||
response = await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
lambda: self.chat_llm.predict(f"The user said: '{user_message}'. Respond helpfully.")
|
||||
)
|
||||
await turn_context.send_activity(response)
|
||||
return
|
||||
|
||||
# Delegate to common logic
|
||||
await self._handle_collected_data(turn_context, user_id, known_values, required_fields, schema)
|
||||
|
||||
async def _handle_collected_data(
|
||||
self,
|
||||
turn_context: TurnContext,
|
||||
user_id: str,
|
||||
known_values: dict,
|
||||
required_fields: list[str],
|
||||
full_schema: dict
|
||||
):
|
||||
missing_fields = [f for f in required_fields if f not in known_values]
|
||||
print(f"Missing fields: {missing_fields}")
|
||||
|
||||
if not missing_fields:
|
||||
try:
|
||||
features = HouseFeatures(**known_values)
|
||||
price = self.price_predictor.predict(features)
|
||||
await turn_context.send_activity(f"The estimated price of the house is ${price:.2f}")
|
||||
del self.user_sessions[user_id]
|
||||
return
|
||||
except ValidationError as e:
|
||||
await turn_context.send_activity(f"Validation failed: {e}")
|
||||
return
|
||||
|
||||
|
||||
# Generate adaptive card for missing fields
|
||||
filtered_schema = {
|
||||
**full_schema,
|
||||
"properties": {
|
||||
k: v for k, v in full_schema["properties"].items() if k in missing_fields
|
||||
},
|
||||
"required": missing_fields
|
||||
}
|
||||
|
||||
card_json = await self.card_bot.generate_card(filtered_schema, known_values)
|
||||
if isinstance(card_json, str):
|
||||
card_json = json.loads(card_json)
|
||||
print(f"card_json: {card_json}")
|
||||
await turn_context.send_activity(
|
||||
Activity(
|
||||
type=ActivityTypes.message,
|
||||
attachments=[
|
||||
Attachment(
|
||||
content_type="application/vnd.microsoft.card.adaptive",
|
||||
content=card_json
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.prompts import ChatPromptTemplate
|
||||
from langchain.chains import LLMChain
|
||||
import asyncio
|
||||
|
||||
class IntentDetector:
|
||||
def __init__(self, temperature: float = 0.0):
|
||||
self.llm = ChatOpenAI(temperature=temperature)
|
||||
self.prompt = ChatPromptTemplate.from_template("""
|
||||
You are an intent detection bot. Classify the user input into one of the following intents:
|
||||
|
||||
- Information about house prices
|
||||
- unknown
|
||||
|
||||
If you're unsure, respond with `unknown`.
|
||||
|
||||
User: {message}
|
||||
Intent:""")
|
||||
self.chain = LLMChain(llm=self.llm, prompt=self.prompt)
|
||||
|
||||
async def detect_intent(self, message: str) -> str:
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(None, self.chain.run, {"message": message})
|
||||
@@ -0,0 +1,31 @@
|
||||
from langchain.chat_models import ChatOpenAI
|
||||
from langchain.prompts import ChatPromptTemplate
|
||||
from langchain.chains import LLMChain
|
||||
from typing import Dict, Any
|
||||
import asyncio
|
||||
|
||||
class SlotFiller:
|
||||
def __init__(self):
|
||||
self.llm = ChatOpenAI(temperature=0)
|
||||
self.prompt = ChatPromptTemplate.from_template("""
|
||||
You are a helpful assistant. Given a message and a schema, extract all known values.
|
||||
|
||||
Only return a JSON object containing the extracted values and no extra text.
|
||||
|
||||
Schema: {schema}
|
||||
Message: {message}
|
||||
""")
|
||||
self.chain = LLMChain(llm=self.llm, prompt=self.prompt)
|
||||
|
||||
async def extract_slots(self, schema: Dict[str, Any], message: str) -> Dict[str, Any]:
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(None, self.chain.run, {
|
||||
"schema": schema,
|
||||
"message": message
|
||||
})
|
||||
|
||||
import json
|
||||
try:
|
||||
return json.loads(result)
|
||||
except Exception:
|
||||
return {}
|
||||
Reference in New Issue
Block a user