Initial commit
This commit is contained in:
@@ -1,37 +1,16 @@
|
|||||||
# Use uma imagem base Python oficial.
|
|
||||||
# Escolha uma versão que seja compatível com suas dependências.
|
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
# Copia os arquivos de requisitos primeiro para aproveitar o cache do Docker
|
|
||||||
COPY requirements.txt ./requirements.txt
|
COPY requirements.txt ./requirements.txt
|
||||||
|
|
||||||
# Instala as dependências do backend
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Copia o restante dos diretórios e arquivos da aplicação
|
|
||||||
|
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
|
|
||||||
# Garante que o script de inicialização seja executável
|
|
||||||
RUN chmod +x ./entrypoint.sh
|
RUN chmod +x ./entrypoint.sh
|
||||||
|
|
||||||
# Cria os diretórios que a API FastAPI pode precisar (se eles não existirem)
|
|
||||||
# Estes diretórios serão usados para persistência se volumes forem montados.
|
|
||||||
#RUN mkdir -p /app/faiss_index_store && \
|
|
||||||
# mkdir -p /app/uploaded_pdfs
|
|
||||||
|
|
||||||
# Expõe as portas que os aplicativos usarão
|
|
||||||
# Porta 8000 para a API FastAPI
|
# Porta 8000 para a API FastAPI
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
# Porta 8501 para o aplicativo Streamlit
|
# Porta 8501 para o aplicativo Streamlit
|
||||||
EXPOSE 8501
|
EXPOSE 8501
|
||||||
|
|
||||||
# Define a variável de ambiente GROQ_API_KEY.
|
|
||||||
# É ALTAMENTE RECOMENDADO passar esta variável em tempo de execução
|
|
||||||
# em vez de embuti-la aqui por questões de segurança.
|
|
||||||
# Exemplo: docker run -e GROQ_API_KEY="sua_chave_aqui" ...
|
|
||||||
# ENV GROQ_API_KEY="SUA_CHAVE_GROQ_AQUI_SE_NECESSARIO_MAS_NAO_RECOMENDADO_EMBUTIR"
|
|
||||||
|
|
||||||
# Comando para executar quando o contêiner iniciar
|
|
||||||
# Executa o script start.sh que gerencia os dois processos
|
|
||||||
CMD ["./entrypoint.sh"]
|
CMD ["./entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from .backend import BDAgent
|
from .backend import orquestrador
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class QueryResponse(BaseModel):
|
|||||||
|
|
||||||
@app.post("/agent", response_model=QueryResponse)
|
@app.post("/agent", response_model=QueryResponse)
|
||||||
def run_agent(request: QueryRequest):
|
def run_agent(request: QueryRequest):
|
||||||
result = BDAgent.main(request.query, request.history, request.model, request.base)
|
result = orquestrador.main(request.query, request.history, request.model, request.base)
|
||||||
return QueryResponse(
|
return QueryResponse(
|
||||||
response=result["response"],
|
response=result["response"],
|
||||||
input_tokens=result["input_tokens"],
|
input_tokens=result["input_tokens"],
|
||||||
|
|||||||
@@ -1,482 +0,0 @@
|
|||||||
"""
|
|
||||||
LangGraph Agent using AWS Bedrock Cross-Region Inference Profile with Tools
|
|
||||||
|
|
||||||
This script demonstrates how to create a LangGraph agent that uses
|
|
||||||
an AWS Bedrock inference profile with custom tools (add and multiply).
|
|
||||||
"""
|
|
||||||
import boto3
|
|
||||||
from typing import TypedDict, Annotated
|
|
||||||
from langgraph.graph import StateGraph, END
|
|
||||||
from langchain_aws import ChatBedrockConverse
|
|
||||||
from langchain_core.tools import tool
|
|
||||||
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage, SystemMessage
|
|
||||||
import operator
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
from langfuse import Langfuse
|
|
||||||
from langfuse.langchain import CallbackHandler
|
|
||||||
from botocore.exceptions import ClientError
|
|
||||||
import os
|
|
||||||
WORKGROUP = "iceberg-workgroup"
|
|
||||||
DATABASE = "dnx_warehouse"
|
|
||||||
TABLE = "poc_dnx_monthly_summary"
|
|
||||||
REGION = "us-east-1"
|
|
||||||
|
|
||||||
# DynamoDB client
|
|
||||||
dynamodb = boto3.resource("dynamodb", region_name=REGION)
|
|
||||||
|
|
||||||
|
|
||||||
@tool
|
|
||||||
def get_monthly_report(id: str, variable: str) -> str:
|
|
||||||
"""
|
|
||||||
Get a specific variable's data from DynamoDB for a specific id.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id: The id of the data
|
|
||||||
variable: The variable/column name to retrieve from the table
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The content of the specified variable for the given id
|
|
||||||
"""
|
|
||||||
print(f"\n🔧 [TOOL CALLED] get_monthly_report for month: {id}, variable: {variable}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
table = dynamodb.Table(TABLE)
|
|
||||||
response = table.get_item(Key={"id": id})
|
|
||||||
|
|
||||||
if "Item" not in response:
|
|
||||||
return f"No report found for month: {id}"
|
|
||||||
|
|
||||||
item = response["Item"]
|
|
||||||
content = item.get(variable, "")
|
|
||||||
|
|
||||||
if not content:
|
|
||||||
return f"Variable '{variable}' not found for month: {id}"
|
|
||||||
|
|
||||||
result = f"<{id}>\n{content}\n</{id}>"
|
|
||||||
return result
|
|
||||||
|
|
||||||
except ClientError as e:
|
|
||||||
error_message = e.response["Error"]["Message"]
|
|
||||||
return f"Error fetching report: {error_message}"
|
|
||||||
|
|
||||||
|
|
||||||
@tool
|
|
||||||
def get_consolidated_keys(id: str) -> str:
|
|
||||||
"""
|
|
||||||
Get the list of consolidated keys (variables) available in the table for a specific month.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id: The id of the data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The list of available variables/keys for the specified data
|
|
||||||
"""
|
|
||||||
print(f"\n🔧 [TOOL CALLED] get_consolidated_keys for id: {id}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
table = dynamodb.Table(TABLE)
|
|
||||||
response = table.get_item(Key={"id": id})
|
|
||||||
|
|
||||||
if "Item" not in response:
|
|
||||||
return f"No data found for month: {id}"
|
|
||||||
|
|
||||||
item = response["Item"]
|
|
||||||
chaves_consolidadas = item.get("chaves_consolidadas", "")
|
|
||||||
|
|
||||||
if not chaves_consolidadas:
|
|
||||||
return f"No consolidated keys found for id: {id}"
|
|
||||||
|
|
||||||
return chaves_consolidadas
|
|
||||||
|
|
||||||
except ClientError as e:
|
|
||||||
error_message = e.response["Error"]["Message"]
|
|
||||||
return f"Error fetching consolidated keys: {error_message}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_contexto() -> dict:
|
|
||||||
"""
|
|
||||||
Get contexto, filter, and items_disponiveis from DynamoDB where id=DASHBOARD+'_contexto'.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with 'contexto', 'filter', and 'items_disponiveis' keys
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
table = dynamodb.Table(TABLE)
|
|
||||||
response = table.get_item(Key={"id": DASHBOARD + "_contexto"})
|
|
||||||
|
|
||||||
if "Item" not in response:
|
|
||||||
return {"contexto": "", "filter": "", "items_disponiveis": {}}
|
|
||||||
|
|
||||||
item = response["Item"]
|
|
||||||
return {
|
|
||||||
"contexto": item.get("contexto", ""),
|
|
||||||
"filter": item.get("filter_key", ""),
|
|
||||||
"items_disponiveis": item.get("itens_disponiveis", {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
except ClientError as e:
|
|
||||||
error_message = e.response["Error"]["Message"]
|
|
||||||
return {"contexto": f"Error: {error_message}", "filter": "", "items_disponiveis": {}}
|
|
||||||
|
|
||||||
|
|
||||||
def get_secret():
|
|
||||||
|
|
||||||
secret_name = "assistente-db-secrets-manager"
|
|
||||||
region_name = "us-east-1"
|
|
||||||
|
|
||||||
# Create a Secrets Manager client
|
|
||||||
session = boto3.session.Session()
|
|
||||||
client = session.client(
|
|
||||||
service_name='secretsmanager',
|
|
||||||
region_name=region_name
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
get_secret_value_response = client.get_secret_value(
|
|
||||||
SecretId=secret_name
|
|
||||||
)
|
|
||||||
except ClientError as e:
|
|
||||||
# For a list of exceptions thrown, see
|
|
||||||
# https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
|
|
||||||
raise e
|
|
||||||
|
|
||||||
secret = get_secret_value_response['SecretString']
|
|
||||||
return secret
|
|
||||||
secrets=json.loads(get_secret())
|
|
||||||
langfuse = Langfuse(
|
|
||||||
public_key=secrets['LANGFUSE-PUBLIC-KEY'],
|
|
||||||
secret_key=secrets['LANGFUSE-SECRET-KEY'],
|
|
||||||
host=os.environ["LANGFUSE_HOST"]
|
|
||||||
)
|
|
||||||
session = boto3.Session()
|
|
||||||
athena = session.client("athena", region_name="us-east-1")
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# QUERY
|
|
||||||
# ==============================================
|
|
||||||
|
|
||||||
def exec_athena_query(query):
|
|
||||||
print("Executando query no Athena...")
|
|
||||||
response = athena.start_query_execution(
|
|
||||||
QueryString=query,
|
|
||||||
QueryExecutionContext={"Database": DATABASE},
|
|
||||||
WorkGroup=WORKGROUP
|
|
||||||
)
|
|
||||||
|
|
||||||
query_execution_id = response["QueryExecutionId"]
|
|
||||||
print(f"QueryExecutionId: {query_execution_id}")
|
|
||||||
|
|
||||||
# ==============================================
|
|
||||||
# AGUARDAR RESULTADO
|
|
||||||
# ==============================================
|
|
||||||
|
|
||||||
while True:
|
|
||||||
result = athena.get_query_execution(QueryExecutionId=query_execution_id)
|
|
||||||
state = result["QueryExecution"]["Status"]["State"]
|
|
||||||
|
|
||||||
if state in ["SUCCEEDED", "FAILED", "CANCELLED"]:
|
|
||||||
print("Estado final:", state)
|
|
||||||
break
|
|
||||||
|
|
||||||
print("Aguardando execução...")
|
|
||||||
time.sleep(1)
|
|
||||||
if state == "SUCCEEDED":
|
|
||||||
output = athena.get_query_results(QueryExecutionId=query_execution_id)
|
|
||||||
print(f"\n🔧 [TOOL CALLED] consult answer")
|
|
||||||
return output["ResultSet"]["Rows"]
|
|
||||||
else:
|
|
||||||
print("Erro ao executar a query.")
|
|
||||||
# Define tools
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Define@tool the agent state
|
|
||||||
class AgentState(TypedDict):
|
|
||||||
messages: Annotated[list, operator.add]
|
|
||||||
current_step: str
|
|
||||||
|
|
||||||
|
|
||||||
# Initialize Bedrock client with inference profile
|
|
||||||
def create_bedrock_llm(model_id: str, region: str = "us-east-1"):
|
|
||||||
"""
|
|
||||||
Create a ChatBedrock instance using a model ID.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model_id: Bedrock model ID (e.g., anthropic.claude-haiku-4-5-20251001-v1:0)
|
|
||||||
region: AWS region (default: us-east-1)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ChatBedrock instance configured with the model
|
|
||||||
"""
|
|
||||||
# Determine provider and model_kwargs based on model ID
|
|
||||||
MODEL_ARNS = {
|
|
||||||
"anthropic.claude-haiku-4-5-20251001-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
||||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
|
||||||
"meta.llama4-maverick-17b-instruct-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.meta.llama4-maverick-17b-instruct-v1:0",
|
|
||||||
"meta.llama4-scout-17b-instruct-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.meta.llama4-scout-17b-instruct-v1:0",
|
|
||||||
"amazon.nova-lite-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.amazon.nova-lite-v1:0",
|
|
||||||
"amazon.nova-pro-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.amazon.nova-pro-v1:0",
|
|
||||||
"amazon.nova-2-lite-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/global.amazon.nova-2-lite-v1:0"
|
|
||||||
}
|
|
||||||
PROVIDER={
|
|
||||||
"anthropic.claude-haiku-4-5-20251001-v1:0": "anthropic",
|
|
||||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": "anthropic",
|
|
||||||
"meta.llama4-maverick-17b-instruct-v1:0": "meta",
|
|
||||||
"meta.llama4-scout-17b-instruct-v1:0": "meta",
|
|
||||||
"amazon.nova-lite-v1:0": "amazon",
|
|
||||||
"amazon.nova-pro-v1:0": "amazon",
|
|
||||||
"amazon.nova-2-lite-v1:0": "amazon"
|
|
||||||
}
|
|
||||||
prefix={
|
|
||||||
"anthropic.claude-haiku-4-5-20251001-v1:0": "us",
|
|
||||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": "global",
|
|
||||||
"meta.llama4-maverick-17b-instruct-v1:0": "us",
|
|
||||||
"meta.llama4-scout-17b-instruct-v1:0": "us",
|
|
||||||
"amazon.nova-lite-v1:0": "us",
|
|
||||||
"amazon.nova-pro-v1:0": "us",
|
|
||||||
"amazon.nova-2-lite-v1:0": "global"
|
|
||||||
}
|
|
||||||
llm = ChatBedrockConverse(
|
|
||||||
model_id=prefix[model_id]+"."+model_id,
|
|
||||||
region_name=region,
|
|
||||||
provider=PROVIDER[model_id],
|
|
||||||
max_tokens=2048,
|
|
||||||
temperature=0.7
|
|
||||||
)
|
|
||||||
|
|
||||||
# Bind tools to the LLM
|
|
||||||
tools = [get_monthly_report, get_consolidated_keys]
|
|
||||||
llm_with_tools = llm.bind_tools(tools)
|
|
||||||
|
|
||||||
return llm_with_tools
|
|
||||||
|
|
||||||
|
|
||||||
# Define agent nodes
|
|
||||||
def call_model(state: AgentState, llm) -> AgentState:
|
|
||||||
"""Call the LLM with tools."""
|
|
||||||
print(f"[MODEL] Calling Bedrock inference profile...")
|
|
||||||
|
|
||||||
messages = state["messages"]
|
|
||||||
response = llm.invoke(messages)
|
|
||||||
state["current_step"] = "model_called"
|
|
||||||
return {"messages": [response]}
|
|
||||||
|
|
||||||
|
|
||||||
def call_tools(state: AgentState) -> AgentState:
|
|
||||||
"""Execute any tool calls from the LLM response."""
|
|
||||||
print(f"[TOOLS] Checking for tool calls...")
|
|
||||||
|
|
||||||
messages = state["messages"]
|
|
||||||
last_message = messages[-1]
|
|
||||||
|
|
||||||
# Check if there are tool calls
|
|
||||||
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
|
||||||
print(f"[TOOLS] Found {len(last_message.tool_calls)} tool call(s)")
|
|
||||||
|
|
||||||
tool_messages = []
|
|
||||||
tools_map = {
|
|
||||||
"get_monthly_report": get_monthly_report,
|
|
||||||
"get_consolidated_keys": get_consolidated_keys
|
|
||||||
}
|
|
||||||
|
|
||||||
# Execute each tool call
|
|
||||||
for tool_call in last_message.tool_calls:
|
|
||||||
tool_name = tool_call["name"]
|
|
||||||
tool_args = tool_call["args"]
|
|
||||||
|
|
||||||
print(f"[TOOLS] Executing: {tool_name}")
|
|
||||||
|
|
||||||
# Call the appropriate tool
|
|
||||||
tool_func = tools_map[tool_name]
|
|
||||||
result = tool_func.invoke(tool_args)
|
|
||||||
|
|
||||||
# Create tool message
|
|
||||||
tool_message = ToolMessage(
|
|
||||||
content=str(result),
|
|
||||||
tool_call_id=tool_call["id"]
|
|
||||||
)
|
|
||||||
tool_messages.append(tool_message)
|
|
||||||
|
|
||||||
state["current_step"] = "tools_executed"
|
|
||||||
return {"messages": tool_messages}
|
|
||||||
else:
|
|
||||||
print(f"[TOOLS] No tool calls found")
|
|
||||||
state["current_step"] = "no_tools"
|
|
||||||
return {"messages": []}
|
|
||||||
|
|
||||||
|
|
||||||
def should_continue(state: AgentState) -> str:
|
|
||||||
"""Determine if we should continue to tools or end."""
|
|
||||||
messages = state["messages"]
|
|
||||||
last_message = messages[-1]
|
|
||||||
|
|
||||||
# If there are tool calls, continue to tools node
|
|
||||||
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
|
||||||
print("[ROUTER] Routing to tools...")
|
|
||||||
return "tools"
|
|
||||||
|
|
||||||
# Otherwise, end
|
|
||||||
print("[ROUTER] No more tool calls, ending...")
|
|
||||||
return "end"
|
|
||||||
|
|
||||||
|
|
||||||
# Build the LangGraph agent
|
|
||||||
def create_agent(inference_profile_arn: str, region: str = "us-east-1"):
|
|
||||||
"""
|
|
||||||
Create a LangGraph agent that uses Bedrock inference profile with tools.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
inference_profile_arn: ARN of the cross-region inference profile
|
|
||||||
region: AWS region
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Compiled LangGraph workflow
|
|
||||||
"""
|
|
||||||
# Initialize the LLM with tools
|
|
||||||
llm = create_bedrock_llm(inference_profile_arn, region)
|
|
||||||
|
|
||||||
# Create the graph
|
|
||||||
workflow = StateGraph(AgentState)
|
|
||||||
|
|
||||||
# Add nodes
|
|
||||||
workflow.add_node("model", lambda state: call_model(state, llm))
|
|
||||||
workflow.add_node("tools", call_tools)
|
|
||||||
|
|
||||||
# Define the workflow
|
|
||||||
workflow.set_entry_point("model")
|
|
||||||
|
|
||||||
# Add conditional edges
|
|
||||||
workflow.add_conditional_edges(
|
|
||||||
"model",
|
|
||||||
should_continue,
|
|
||||||
{
|
|
||||||
"tools": "tools",
|
|
||||||
"end": END
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# After tools, go back to model
|
|
||||||
workflow.add_edge("tools", "model")
|
|
||||||
|
|
||||||
# Compile the graph
|
|
||||||
app = workflow.compile()
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
def main(user_query,history,model,base):
|
|
||||||
"""Main execution function."""
|
|
||||||
global DASHBOARD
|
|
||||||
DASHBOARD = base
|
|
||||||
|
|
||||||
# Configuration - Update with your actual inference profile ARN
|
|
||||||
|
|
||||||
INFERENCE_PROFILE_ARN = model
|
|
||||||
REGION = "us-east-1"
|
|
||||||
# System prompt for the agent
|
|
||||||
|
|
||||||
contexto_data = get_contexto()
|
|
||||||
if contexto_data["filter"]=="period":
|
|
||||||
CONSULT_RULES="""To use the tools you must give the id of the correspondant data, which can be associated to a given month and year in the following format year_month, which:
|
|
||||||
-Year is the year in 4 digits (2025,2024,2023,2022,2021,...)
|
|
||||||
-Month is th two digit representation: 01,02,03,04,05,06,07,08,09,10,11,12
|
|
||||||
The format of the dict is: {id1:year_month1,id2:year_month2...}
|
|
||||||
Choose the correct id based on the following dict:
|
|
||||||
"""
|
|
||||||
elif contexto_data["filter"]=="event":
|
|
||||||
CONSULT_RULES="""To use the tools you must give the id of the correspondant data, which can be associated to a event, which is in the format "Name - City DD/MM/YYYY", where the last is a date in the format day/month/year. Theformat of elements in dict is {id1:event_description1,id2:event_description2...}"""
|
|
||||||
else:
|
|
||||||
CONSULT_RULES="""Wrong filter value, you must terminate the workflow and ask the user to contact the technical team"""
|
|
||||||
SYSTEM_PROMPT=""" You are a analitical agent in Brazilian Portuguese, with acess to monthly reports about a specific company, specified in the context. You have access to tools that lets you consult present variables in table, you always have access to "context", which keeps inside answers to different questions, that you may consult as you desire.
|
|
||||||
Do not access other variables besides the ones reported by the tool and "context".
|
|
||||||
You currently have access to data in a period specified in the context, so only answer questions inside the time window.
|
|
||||||
<context>
|
|
||||||
"""+contexto_data["contexto"]+"""
|
|
||||||
</context>
|
|
||||||
"""+CONSULT_RULES+"""
|
|
||||||
<correlation>
|
|
||||||
"""+str(contexto_data["items_disponiveis"])+"""
|
|
||||||
</correlation>
|
|
||||||
Here is the chat history:"""+history+"""
|
|
||||||
Inside the "NPS" in data is some useful values to calculate the NPS, which includes "distribuicao".
|
|
||||||
Inside of it are grades and the amount of people who given that grade.
|
|
||||||
Grades from 0 to 6 are detractors.
|
|
||||||
Grades from 7 to 8 are neutral.
|
|
||||||
Grades from 9 to 10 are promoters.
|
|
||||||
Calculate the percentage of them when prompted about NPS and then calculate the nps using the following formula: NPS = %promoter - %detractor, never use the medium of the notes.
|
|
||||||
|
|
||||||
You have access to the tools:
|
|
||||||
-get_consolidated_keys: Given a id returns the column names inside of a entity of a given table element.
|
|
||||||
- get_monthly_report: given a id and a variable name, either one listed in the previous tool output or "context", returns its value. Using "context" gives you a summarization of many answers of questions asked to the customers.
|
|
||||||
Answer, in Brazilian Portuguese, to the user the best you can with the given information, if you don't know the answer or how to answer say so, only answer from what you know.
|
|
||||||
Always consult the most recent information when a date is not given, like questions "Quanto é meu nps?" """
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print("LangGraph Agent with AWS Bedrock Inference Profile + Tools")
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"\nUsing inference profile: {INFERENCE_PROFILE_ARN}")
|
|
||||||
print(f"Region: {REGION}\n")
|
|
||||||
print("Available Tools:")
|
|
||||||
print(" - add_numbers(a, b): Add two numbers")
|
|
||||||
print(" - multiply_numbers(a, b): Multiply two numbers")
|
|
||||||
print("\nSystem Prompt: Configured ✓")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Create the agent with a unique session_id to group all steps
|
|
||||||
langfuse_handler = CallbackHandler()
|
|
||||||
agent = create_agent(INFERENCE_PROFILE_ARN, REGION)
|
|
||||||
|
|
||||||
# Initialize state with system prompt
|
|
||||||
initial_state = {
|
|
||||||
"messages": [
|
|
||||||
SystemMessage(content=SYSTEM_PROMPT),
|
|
||||||
HumanMessage(content=user_query)
|
|
||||||
],
|
|
||||||
"current_step": "init"
|
|
||||||
}
|
|
||||||
|
|
||||||
print(f"\nUser Query: {user_query}\n")
|
|
||||||
print("-" * 60)
|
|
||||||
|
|
||||||
# Run the agent with callbacks at graph level
|
|
||||||
config = {"callbacks": [langfuse_handler], "tags": [DASHBOARD]}
|
|
||||||
final_state = agent.invoke(initial_state, config=config)
|
|
||||||
|
|
||||||
# Display results
|
|
||||||
print("-" * 60)
|
|
||||||
print("\n[FINAL RESULT]")
|
|
||||||
print("\nConversation History:")
|
|
||||||
for i, msg in enumerate(final_state["messages"], 1):
|
|
||||||
if isinstance(msg, SystemMessage):
|
|
||||||
print(f"\n{i}. System: [System prompt configured]")
|
|
||||||
elif isinstance(msg, HumanMessage):
|
|
||||||
print(f"\n{i}. User: {msg.content}")
|
|
||||||
elif isinstance(msg, AIMessage):
|
|
||||||
if hasattr(msg, 'tool_calls') and msg.tool_calls:
|
|
||||||
print(f"\n{i}. AI: [Calling tools...]")
|
|
||||||
else:
|
|
||||||
print(f"\n{i}. AI: {msg.content}")
|
|
||||||
elif isinstance(msg, ToolMessage):
|
|
||||||
print(f"\n{i}. Tool Result: {msg.content}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print(f"Agent completed successfully. Final step: {final_state['current_step']}")
|
|
||||||
|
|
||||||
# Aggregate token usage from all AIMessage objects
|
|
||||||
total_input_tokens = 0
|
|
||||||
total_output_tokens = 0
|
|
||||||
for msg in final_state["messages"]:
|
|
||||||
if isinstance(msg, AIMessage) and hasattr(msg, 'usage_metadata') and msg.usage_metadata:
|
|
||||||
total_input_tokens += msg.usage_metadata.get("input_tokens", 0)
|
|
||||||
total_output_tokens += msg.usage_metadata.get("output_tokens", 0)
|
|
||||||
|
|
||||||
langfuse.flush()
|
|
||||||
return {
|
|
||||||
"response": final_state['messages'][-1].content,
|
|
||||||
"input_tokens": total_input_tokens,
|
|
||||||
"output_tokens": total_output_tokens,
|
|
||||||
"total_tokens": total_input_tokens + total_output_tokens,
|
|
||||||
}
|
|
||||||
if __name__=="__main__":
|
|
||||||
main("Liste o nps mês a mês desde maio 2025 até dezembro 2025","","anthropic.claude-sonnet-4-5-20250929-v1:0")
|
|
||||||
0
code/app/backend/__init__.py
Normal file
0
code/app/backend/__init__.py
Normal file
120
code/app/backend/agent_bedrock.py
Normal file
120
code/app/backend/agent_bedrock.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import operator
|
||||||
|
from typing import TypedDict, Annotated
|
||||||
|
|
||||||
|
from langchain_aws import ChatBedrockConverse
|
||||||
|
from langchain_core.messages import AIMessage, ToolMessage
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
|
||||||
|
from .config import REGION, AWS_ACCOUNT
|
||||||
|
|
||||||
|
|
||||||
|
class AgentState(TypedDict):
|
||||||
|
messages: Annotated[list, operator.add]
|
||||||
|
current_step: str
|
||||||
|
|
||||||
|
|
||||||
|
def create_bedrock_llm(model_id: str, region: str = REGION, tools: list = None):
|
||||||
|
"""
|
||||||
|
Create a ChatBedrock instance using a model ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_id: Bedrock model ID (e.g., anthropic.claude-haiku-4-5-20251001-v1:0)
|
||||||
|
region: AWS region (default: REGION env var)
|
||||||
|
tools: List of LangChain tools to bind to the model
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ChatBedrock instance configured with the model
|
||||||
|
"""
|
||||||
|
MODEL_ARNS = {
|
||||||
|
"anthropic.claude-haiku-4-5-20251001-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||||
|
"anthropic.claude-sonnet-4-5-20250929-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||||
|
"meta.llama4-maverick-17b-instruct-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.meta.llama4-maverick-17b-instruct-v1:0",
|
||||||
|
"meta.llama4-scout-17b-instruct-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.meta.llama4-scout-17b-instruct-v1:0",
|
||||||
|
"amazon.nova-lite-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.amazon.nova-lite-v1:0",
|
||||||
|
"amazon.nova-pro-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.amazon.nova-pro-v1:0",
|
||||||
|
"amazon.nova-2-lite-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/global.amazon.nova-2-lite-v1:0",
|
||||||
|
}
|
||||||
|
PROVIDER = {
|
||||||
|
"anthropic.claude-haiku-4-5-20251001-v1:0": "anthropic",
|
||||||
|
"anthropic.claude-sonnet-4-5-20250929-v1:0": "anthropic",
|
||||||
|
"meta.llama4-maverick-17b-instruct-v1:0": "meta",
|
||||||
|
"meta.llama4-scout-17b-instruct-v1:0": "meta",
|
||||||
|
"amazon.nova-lite-v1:0": "amazon",
|
||||||
|
"amazon.nova-pro-v1:0": "amazon",
|
||||||
|
"amazon.nova-2-lite-v1:0": "amazon",
|
||||||
|
}
|
||||||
|
prefix = {
|
||||||
|
"anthropic.claude-haiku-4-5-20251001-v1:0": "us",
|
||||||
|
"anthropic.claude-sonnet-4-5-20250929-v1:0": "global",
|
||||||
|
"meta.llama4-maverick-17b-instruct-v1:0": "us",
|
||||||
|
"meta.llama4-scout-17b-instruct-v1:0": "us",
|
||||||
|
"amazon.nova-lite-v1:0": "us",
|
||||||
|
"amazon.nova-pro-v1:0": "us",
|
||||||
|
"amazon.nova-2-lite-v1:0": "global",
|
||||||
|
}
|
||||||
|
llm = ChatBedrockConverse(
|
||||||
|
model_id=prefix[model_id] + "." + model_id,
|
||||||
|
region_name=region,
|
||||||
|
provider=PROVIDER[model_id],
|
||||||
|
max_tokens=2048,
|
||||||
|
temperature=0.7,
|
||||||
|
)
|
||||||
|
return llm.bind_tools(tools or [])
|
||||||
|
|
||||||
|
|
||||||
|
def call_model(state: AgentState, llm) -> AgentState:
|
||||||
|
"""Call the LLM with tools."""
|
||||||
|
response = llm.invoke(state["messages"])
|
||||||
|
state["current_step"] = "model_called"
|
||||||
|
return {"messages": [response]}
|
||||||
|
|
||||||
|
|
||||||
|
def call_tools(state: AgentState, tools_map: dict) -> AgentState:
|
||||||
|
"""Execute any tool calls from the LLM response."""
|
||||||
|
last_message = state["messages"][-1]
|
||||||
|
|
||||||
|
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
||||||
|
tool_messages = []
|
||||||
|
for tool_call in last_message.tool_calls:
|
||||||
|
result = tools_map[tool_call["name"]].invoke(tool_call["args"])
|
||||||
|
tool_messages.append(ToolMessage(content=str(result), tool_call_id=tool_call["id"]))
|
||||||
|
|
||||||
|
state["current_step"] = "tools_executed"
|
||||||
|
return {"messages": tool_messages}
|
||||||
|
else:
|
||||||
|
state["current_step"] = "no_tools"
|
||||||
|
return {"messages": []}
|
||||||
|
|
||||||
|
|
||||||
|
def should_continue(state: AgentState) -> str:
|
||||||
|
"""Determine if we should continue to tools or end."""
|
||||||
|
last_message = state["messages"][-1]
|
||||||
|
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
||||||
|
return "tools"
|
||||||
|
return "end"
|
||||||
|
|
||||||
|
|
||||||
|
def create_agent(inference_profile_arn: str, region: str = REGION, tools: list = None):
|
||||||
|
"""
|
||||||
|
Create a LangGraph agent that uses Bedrock inference profile with tools.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
inference_profile_arn: ARN of the cross-region inference profile
|
||||||
|
region: AWS region
|
||||||
|
tools: List of LangChain tools to bind to the model
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Compiled LangGraph workflow
|
||||||
|
"""
|
||||||
|
tools = tools or []
|
||||||
|
llm = create_bedrock_llm(inference_profile_arn, region, tools)
|
||||||
|
tools_map = {t.name: t for t in tools}
|
||||||
|
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
workflow.add_node("model", lambda state: call_model(state, llm))
|
||||||
|
workflow.add_node("tools", lambda state: call_tools(state, tools_map))
|
||||||
|
workflow.set_entry_point("model")
|
||||||
|
workflow.add_conditional_edges("model", should_continue, {"tools": "tools", "end": END})
|
||||||
|
workflow.add_edge("tools", "model")
|
||||||
|
|
||||||
|
return workflow.compile()
|
||||||
6
code/app/backend/config.py
Normal file
6
code/app/backend/config.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
TABLE = os.environ["TABLE"]
|
||||||
|
REGION = os.environ["REGION"]
|
||||||
|
AWS_ACCOUNT = os.environ["AWS_ACCOUNT"]
|
||||||
|
SECRET_NAME = os.environ["SECRET_NAME"]
|
||||||
53
code/app/backend/dynamo.py
Normal file
53
code/app/backend/dynamo.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import boto3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from langfuse import Langfuse
|
||||||
|
|
||||||
|
from .config import REGION, TABLE, SECRET_NAME
|
||||||
|
|
||||||
|
dynamodb = boto3.resource("dynamodb", region_name=REGION)
|
||||||
|
|
||||||
|
|
||||||
|
def get_secret() -> str:
|
||||||
|
session = boto3.session.Session()
|
||||||
|
client = session.client(service_name="secretsmanager", region_name=REGION)
|
||||||
|
try:
|
||||||
|
response = client.get_secret_value(SecretId=SECRET_NAME)
|
||||||
|
except ClientError as e:
|
||||||
|
raise e
|
||||||
|
return response["SecretString"]
|
||||||
|
|
||||||
|
|
||||||
|
secrets = json.loads(get_secret())
|
||||||
|
langfuse = Langfuse(
|
||||||
|
public_key=secrets["LANGFUSE-PUBLIC-KEY"],
|
||||||
|
secret_key=secrets["LANGFUSE-SECRET-KEY"],
|
||||||
|
host=os.environ["LANGFUSE_HOST"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_contexto(dashboard: str) -> dict:
|
||||||
|
"""
|
||||||
|
Get contexto, filter, and items_disponiveis from DynamoDB for a given dashboard.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with 'contexto', 'filter', and 'items_disponiveis' keys
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
table = dynamodb.Table(TABLE)
|
||||||
|
response = table.get_item(Key={"id": dashboard + "_contexto"})
|
||||||
|
|
||||||
|
if "Item" not in response:
|
||||||
|
return {"contexto": "", "filter": "", "items_disponiveis": {}}
|
||||||
|
|
||||||
|
item = response["Item"]
|
||||||
|
return {
|
||||||
|
"contexto": item.get("contexto", ""),
|
||||||
|
"filter": item.get("filter_key", ""),
|
||||||
|
"items_disponiveis": item.get("itens_disponiveis", {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
except ClientError as e:
|
||||||
|
error_message = e.response["Error"]["Message"]
|
||||||
|
return {"contexto": f"Error: {error_message}", "filter": "", "items_disponiveis": {}}
|
||||||
96
code/app/backend/orquestrador.py
Normal file
96
code/app/backend/orquestrador.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
||||||
|
from langfuse.langchain import CallbackHandler
|
||||||
|
|
||||||
|
from .config import REGION
|
||||||
|
from .dynamo import langfuse, get_contexto
|
||||||
|
from .agent_bedrock import create_agent
|
||||||
|
from .tools import ReportTools
|
||||||
|
|
||||||
|
|
||||||
|
def main(user_query, history, model, base):
|
||||||
|
"""Main execution function."""
|
||||||
|
contexto_data = get_contexto(base)
|
||||||
|
|
||||||
|
id_mapping = {
|
||||||
|
f"id_{i}": real_id
|
||||||
|
for i, real_id in enumerate(contexto_data["items_disponiveis"], 1)
|
||||||
|
}
|
||||||
|
local_items = {
|
||||||
|
local_id: contexto_data["items_disponiveis"][real_id]
|
||||||
|
for local_id, real_id in id_mapping.items()
|
||||||
|
}
|
||||||
|
report_tools = ReportTools(id_mapping)
|
||||||
|
|
||||||
|
if contexto_data["filter"] == "period":
|
||||||
|
CONSULT_RULES = """To use the tools you must give the id of the correspondant data, which can be associated to a given month and year in the following format year_month, which:
|
||||||
|
-Year is the year in 4 digits (2025,2024,2023,2022,2021,...)
|
||||||
|
-Month is th two digit representation: 01,02,03,04,05,06,07,08,09,10,11,12
|
||||||
|
The format of the dict is: {id1:year_month1,id2:year_month2...}
|
||||||
|
Choose the correct id based on the following dict:
|
||||||
|
"""
|
||||||
|
elif contexto_data["filter"] == "event":
|
||||||
|
CONSULT_RULES = """To use the tools you must give the id of the correspondant data, which can be associated to a event, which is in the format "Name - City DD/MM/YYYY", where the last is a date in the format day/month/year. Theformat of elements in dict is {id1:event_description1,id2:event_description2...}"""
|
||||||
|
else:
|
||||||
|
CONSULT_RULES = """Wrong filter value, you must terminate the workflow and ask the user to contact the technical team"""
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = """ You are a analitical agent in Brazilian Portuguese, with acess to monthly reports about a specific company, specified in the context. You have access to tools that lets you consult present variables in table, you always have access to "context", which keeps inside answers to different questions, that you may consult as you desire.
|
||||||
|
Do not access other variables besides the ones reported by the tool and "context".
|
||||||
|
You currently have access to data in a period specified in the context, so only answer questions inside the time window.
|
||||||
|
<context>
|
||||||
|
""" + contexto_data["contexto"] + """
|
||||||
|
</context>
|
||||||
|
""" + CONSULT_RULES + """
|
||||||
|
<correlation>
|
||||||
|
""" + str(local_items) + """
|
||||||
|
</correlation>
|
||||||
|
Here is the chat history:""" + history + """
|
||||||
|
Inside the "NPS" in data is some useful values to calculate the NPS, which includes "distribuicao".
|
||||||
|
Inside of it are grades and the amount of people who given that grade.
|
||||||
|
Grades from 0 to 6 are detractors.
|
||||||
|
Grades from 7 to 8 are neutral.
|
||||||
|
Grades from 9 to 10 are promoters.
|
||||||
|
Calculate the percentage of them when prompted about NPS and then calculate the nps using the following formula: NPS = %promoter - %detractor, never use the medium of the notes.
|
||||||
|
|
||||||
|
You have access to the tools:
|
||||||
|
-get_consolidated_keys: Given a id returns the column names inside of a entity of a given table element.
|
||||||
|
- get_monthly_report: given a id and a variable name, either one listed in the previous tool output or "context", returns its value. Using "context" gives you a summarization of many answers of questions asked to the customers.
|
||||||
|
Answer, in Brazilian Portuguese, to the user the best you can with the given information, if you don't know the answer or how to answer say so, only answer from what you know.
|
||||||
|
Always consult the most recent information when a date is not given, like questions "Quanto é meu nps?" """
|
||||||
|
|
||||||
|
langfuse_handler = CallbackHandler()
|
||||||
|
agent = create_agent(model, REGION, tools=report_tools.as_tools())
|
||||||
|
|
||||||
|
initial_state = {
|
||||||
|
"messages": [
|
||||||
|
SystemMessage(content=SYSTEM_PROMPT),
|
||||||
|
HumanMessage(content=user_query),
|
||||||
|
],
|
||||||
|
"current_step": "init",
|
||||||
|
}
|
||||||
|
|
||||||
|
config = {"callbacks": [langfuse_handler], "tags": [base]}
|
||||||
|
final_state = agent.invoke(initial_state, config=config)
|
||||||
|
|
||||||
|
total_input_tokens = 0
|
||||||
|
total_output_tokens = 0
|
||||||
|
for msg in final_state["messages"]:
|
||||||
|
if isinstance(msg, AIMessage) and hasattr(msg, "usage_metadata") and msg.usage_metadata:
|
||||||
|
total_input_tokens += msg.usage_metadata.get("input_tokens", 0)
|
||||||
|
total_output_tokens += msg.usage_metadata.get("output_tokens", 0)
|
||||||
|
|
||||||
|
langfuse.flush()
|
||||||
|
return {
|
||||||
|
"response": final_state["messages"][-1].content,
|
||||||
|
"input_tokens": total_input_tokens,
|
||||||
|
"output_tokens": total_output_tokens,
|
||||||
|
"total_tokens": total_input_tokens + total_output_tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main(
|
||||||
|
"Liste o nps mês a mês desde maio 2025 até dezembro 2025",
|
||||||
|
"",
|
||||||
|
"anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||||
|
"bacio_transacional_loja_app",
|
||||||
|
)
|
||||||
85
code/app/backend/tools.py
Normal file
85
code/app/backend/tools.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from langchain_core.tools import StructuredTool
|
||||||
|
|
||||||
|
from .config import TABLE
|
||||||
|
from .dynamo import dynamodb
|
||||||
|
|
||||||
|
|
||||||
|
class ReportTools:
|
||||||
|
def __init__(self, id_mapping: dict[str, str]):
|
||||||
|
self.id_mapping = id_mapping
|
||||||
|
|
||||||
|
def get_variable_value(self, id: str, variable: str) -> str:
|
||||||
|
"""
|
||||||
|
Get a specific variable's value from DynamoDB for a specific id.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The id of the data
|
||||||
|
variable: The variable/column name to retrieve from the table
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The content of the specified variable for the given id
|
||||||
|
"""
|
||||||
|
real_id = self.id_mapping.get(id, id)
|
||||||
|
try:
|
||||||
|
table = dynamodb.Table(TABLE)
|
||||||
|
response = table.get_item(Key={"id": real_id})
|
||||||
|
|
||||||
|
if "Item" not in response:
|
||||||
|
return f"No report found for month: {id}"
|
||||||
|
|
||||||
|
item = response["Item"]
|
||||||
|
content = item.get(variable, "")
|
||||||
|
|
||||||
|
if not content:
|
||||||
|
return f"Variable '{variable}' not found for month: {id}"
|
||||||
|
|
||||||
|
return f"<{id}>\n{content}\n</{id}>"
|
||||||
|
|
||||||
|
except ClientError as e:
|
||||||
|
error_message = e.response["Error"]["Message"]
|
||||||
|
return f"Error fetching report: {error_message}"
|
||||||
|
|
||||||
|
def get_variables_list(self, id: str) -> str:
|
||||||
|
"""
|
||||||
|
Get the list of variables available in the table for a specific month.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id: The id of the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The list of available variables/keys for the specified data
|
||||||
|
"""
|
||||||
|
real_id = self.id_mapping.get(id, id)
|
||||||
|
try:
|
||||||
|
table = dynamodb.Table(TABLE)
|
||||||
|
response = table.get_item(Key={"id": real_id})
|
||||||
|
|
||||||
|
if "Item" not in response:
|
||||||
|
return f"No data found for month: {id}"
|
||||||
|
|
||||||
|
item = response["Item"]
|
||||||
|
chaves_consolidadas = item.get("chaves_consolidadas", "")
|
||||||
|
|
||||||
|
if not chaves_consolidadas:
|
||||||
|
return f"No consolidated keys found for id: {id}"
|
||||||
|
|
||||||
|
return chaves_consolidadas
|
||||||
|
|
||||||
|
except ClientError as e:
|
||||||
|
error_message = e.response["Error"]["Message"]
|
||||||
|
return f"Error fetching consolidated keys: {error_message}"
|
||||||
|
|
||||||
|
def as_tools(self) -> list:
|
||||||
|
return [
|
||||||
|
StructuredTool.from_function(
|
||||||
|
self.get_variable_value,
|
||||||
|
name="get_variable_value",
|
||||||
|
description="Get a specific variable's data from DynamoDB for a specific id.",
|
||||||
|
),
|
||||||
|
StructuredTool.from_function(
|
||||||
|
self.get_variables_list,
|
||||||
|
name="get_variable_list",
|
||||||
|
description="Get the list of variables available in the table for a specific id.",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
import time
|
import time
|
||||||
from backend import BDAgent
|
from backend import orquestrador
|
||||||
import boto3
|
import boto3
|
||||||
from boto3.dynamodb.conditions import Key
|
from boto3.dynamodb.conditions import Key
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ st.set_page_config(
|
|||||||
)
|
)
|
||||||
|
|
||||||
session = boto3.Session()
|
session = boto3.Session()
|
||||||
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
|
dynamodb = boto3.resource("dynamodb", region_name=orquestrador.REGION)
|
||||||
|
|
||||||
def list_bases():
|
def list_bases():
|
||||||
table = dynamodb.Table("poc_dnx_monthly_summary")
|
table = dynamodb.Table("poc_dnx_monthly_summary")
|
||||||
@@ -74,7 +74,7 @@ if prompt := st.chat_input("Type your message here..."):
|
|||||||
|
|
||||||
# Simulate streaming response (replace with actual API call)
|
# Simulate streaming response (replace with actual API call)
|
||||||
|
|
||||||
result = BDAgent.main(prompt,str(st.session_state.messages),selected_value,base)
|
result = orquestrador.main(prompt,str(st.session_state.messages),selected_value,base)
|
||||||
full_response = result["response"]
|
full_response = result["response"]
|
||||||
|
|
||||||
# Simulate typing effect
|
# Simulate typing effect
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
boto3>=1.34.0
|
boto3==1.42.10
|
||||||
langchain-aws>=0.1.0
|
langchain-aws==1.1.0
|
||||||
langgraph>=0.0.20
|
langgraph==1.0.5
|
||||||
langchain>=0.1.0
|
langchain==1.2.0
|
||||||
streamlit
|
streamlit==1.52.2
|
||||||
langfuse
|
langfuse==3.11.2
|
||||||
fastapi
|
fastapi==0.129.0
|
||||||
uvicorn
|
uvicorn==0.41.0
|
||||||
|
|||||||
52
docs/README.md
Normal file
52
docs/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Assistente Analítico para Banco de Dados - Documentação
|
||||||
|
|
||||||
|
Documentação do projeto **Assistente Analítico para Banco de Dados** da Inovyo, uma plataforma de IA conversacional que utiliza dados de pesquisas de experiência do cliente (CX) para fornecer insights analíticos.
|
||||||
|
|
||||||
|
## Índice
|
||||||
|
|
||||||
|
| Documento | Descrição |
|
||||||
|
|-----------|-----------|
|
||||||
|
| [Arquitetura](architecture.md) | Visão geral da arquitetura, componentes e fluxo de dados |
|
||||||
|
| [Referência da API](api-reference.md) | Endpoints, modelos de dados e exemplos de uso |
|
||||||
|
| [Modelo de Dados](data-model.md) | Estrutura das tabelas, relacionamentos e convenções |
|
||||||
|
| [Infraestrutura](infrastructure.md) | Recursos AWS provisionados com Pulumi (ECS, ALB, ECR) |
|
||||||
|
| [Deploy](deployment.md) | Processo de build, push e deploy da aplicação |
|
||||||
|
| [Desenvolvimento](development.md) | Guia para contribuição, estrutura de código e padrões |
|
||||||
|
|
||||||
|
## Visão Geral do Projeto
|
||||||
|
|
||||||
|
A Inovyo é uma plataforma especializada em gestão de experiência do cliente (CX), responsável por coletar, processar e analisar dados de pesquisas. Este projeto cria um chatbot que atua como **consultor especialista**, utilizando os dados dos dashboards da Inovyo para:
|
||||||
|
|
||||||
|
- Responder perguntas sobre resultados de pesquisas
|
||||||
|
- Identificar padrões, tendências e oportunidades
|
||||||
|
- Auxiliar gestores na interpretação de indicadores (NPS, CSAT, CES)
|
||||||
|
- Oferecer insights contextualizados e acionáveis
|
||||||
|
|
||||||
|
## Stack Tecnológico
|
||||||
|
|
||||||
|
| Camada | Tecnologia |
|
||||||
|
|--------|-----------|
|
||||||
|
| **Linguagem** | Python 3.12+ |
|
||||||
|
| **Frontend** | Streamlit |
|
||||||
|
| **API** | FastAPI + Uvicorn |
|
||||||
|
| **Agente IA** | LangGraph + LangChain |
|
||||||
|
| **LLMs** | AWS Bedrock (Claude, Llama, Nova) |
|
||||||
|
| **Banco de Dados** | DynamoDB |
|
||||||
|
| **Observabilidade** | Langfuse |
|
||||||
|
| **Infraestrutura** | AWS ECS Fargate, ALB, ECR |
|
||||||
|
| **IaC** | Pulumi (Python) |
|
||||||
|
| **Container** | Docker |
|
||||||
|
|
||||||
|
## Estrutura do Backend
|
||||||
|
|
||||||
|
O pacote `backend` é dividido em módulos com responsabilidades únicas:
|
||||||
|
|
||||||
|
```
|
||||||
|
code/app/backend/
|
||||||
|
├── __init__.py # Marca o diretório como pacote Python
|
||||||
|
├── config.py # Leitura de variáveis de ambiente
|
||||||
|
├── dynamo.py # Cliente DynamoDB, Langfuse, get_contexto
|
||||||
|
├── tools.py # Ferramentas LangChain (@tool)
|
||||||
|
├── agent_bedrock.py # LLM Bedrock, grafo LangGraph, AgentState
|
||||||
|
└── orquestrador.py # Ponto de entrada: main()
|
||||||
|
```
|
||||||
88
docs/api-reference.md
Normal file
88
docs/api-reference.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Referência da API
|
||||||
|
|
||||||
|
A API REST é servida pelo FastAPI na porta `8000`, implementada em [app/api.py](../code/app/api.py) e orquestrada por [app/backend/orquestrador.py](../code/app/backend/orquestrador.py).
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### `GET /`
|
||||||
|
|
||||||
|
Health check da aplicação.
|
||||||
|
|
||||||
|
**Resposta:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `POST /agent`
|
||||||
|
|
||||||
|
Executa uma consulta no agente analítico.
|
||||||
|
|
||||||
|
**Request Body (`QueryRequest`):**
|
||||||
|
|
||||||
|
| Campo | Tipo | Obrigatório | Descrição |
|
||||||
|
|-------|------|-------------|-----------|
|
||||||
|
| `query` | `string` | Sim | Pergunta do usuário em português |
|
||||||
|
| `history` | `string` | Não | Histórico de mensagens anteriores (serializado) |
|
||||||
|
| `model` | `string` | Não | ID do modelo LLM (padrão: Claude Haiku) |
|
||||||
|
| `base` | `string` | Não | Nome do dashboard/base de dados |
|
||||||
|
|
||||||
|
**Response Body (`QueryResponse`):**
|
||||||
|
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| `response` | `string` | Resposta do agente em português |
|
||||||
|
| `input_tokens` | `int` | Total de tokens de entrada consumidos |
|
||||||
|
| `output_tokens` | `int` | Total de tokens de saída gerados |
|
||||||
|
| `total_tokens` | `int` | Total de tokens (input + output) |
|
||||||
|
|
||||||
|
**Exemplo — cURL:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/agent \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"query": "Qual foi meu NPS?",
|
||||||
|
"history": "",
|
||||||
|
"model": "anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||||
|
"base": "nome_do_dashboard"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exemplo — Resposta:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"response": "O NPS foi...",
|
||||||
|
"input_tokens": 1250,
|
||||||
|
"output_tokens": 340,
|
||||||
|
"total_tokens": 1590
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modelos Disponíveis
|
||||||
|
|
||||||
|
| ID do Modelo | Provider |
|
||||||
|
|-------------|----------|
|
||||||
|
| `anthropic.claude-haiku-4-5-20251001-v1:0` | Anthropic |
|
||||||
|
| `anthropic.claude-sonnet-4-5-20250929-v1:0` | Anthropic |
|
||||||
|
| `meta.llama4-maverick-17b-instruct-v1:0` | Meta |
|
||||||
|
| `meta.llama4-scout-17b-instruct-v1:0` | Meta |
|
||||||
|
| `amazon.nova-lite-v1:0` | Amazon |
|
||||||
|
| `amazon.nova-pro-v1:0` | Amazon |
|
||||||
|
| `amazon.nova-2-lite-v1:0` | Amazon |
|
||||||
|
|
||||||
|
## Bases Disponíveis
|
||||||
|
|
||||||
|
As bases são carregadas dinamicamente do DynamoDB (tabela `TABLE`, index `item_type_index`, `item_type = "contexto"`). O formato do nome segue o padrão `{cliente}_{dashboard}`.
|
||||||
|
|
||||||
|
## Documentação Interativa
|
||||||
|
|
||||||
|
Com a aplicação rodando:
|
||||||
|
|
||||||
|
- **Swagger UI:** `http://localhost:8000/docs`
|
||||||
|
- **ReDoc:** `http://localhost:8000/redoc`
|
||||||
164
docs/architecture.md
Normal file
164
docs/architecture.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Arquitetura
|
||||||
|
|
||||||
|
## Visão Geral
|
||||||
|
|
||||||
|
O Assistente Analítico é composto por três camadas principais: **interface** (Streamlit e FastAPI), **agente de IA** (LangGraph com Bedrock) e **dados** (DynamoDB).
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ Interfaces │
|
||||||
|
│ ┌────────────────────┐ ┌────────────────────────┐ │
|
||||||
|
│ │ Streamlit (8501) │ │ FastAPI (8000) │ │
|
||||||
|
│ │ front.py │ │ api.py │ │
|
||||||
|
│ └────────┬───────────┘ └──────────┬─────────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └──────────┬──────────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ orquestrador.main(query, history, model, base) │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────────▼──────────────────────────┐ │
|
||||||
|
│ │ agent_bedrock — LangGraph │ │
|
||||||
|
│ │ ┌────────┐ ┌───────┐ ┌────────────┐ │ │
|
||||||
|
│ │ │ Model │───▶│Router │───▶│ Tools │ │ │
|
||||||
|
│ │ │ Node │◀───│ │ │ Node │ │ │
|
||||||
|
│ │ └────────┘ └───────┘ └────────────┘ │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────┐ │
|
||||||
|
│ │ Serviços AWS │ │
|
||||||
|
│ │ ┌──────────┐ ┌────────────────────────────┐ │ │
|
||||||
|
│ │ │ Bedrock │ │ DynamoDB │ │ │
|
||||||
|
│ │ │ (LLMs) │ │ (contexto + dados pré- │ │ │
|
||||||
|
│ │ └──────────┘ │ processados) │ │ │
|
||||||
|
│ │ └────────────────────────────┘ │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────────┐ │
|
||||||
|
│ │ Observabilidade │ │
|
||||||
|
│ │ ┌──────────┐ ┌──────────────────────────┐ │ │
|
||||||
|
│ │ │ Langfuse │ │ CloudWatch Logs │ │ │
|
||||||
|
│ │ └──────────┘ └──────────────────────────┘ │ │
|
||||||
|
│ └──────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Módulos do Backend
|
||||||
|
|
||||||
|
O pacote `backend` é organizado por responsabilidade:
|
||||||
|
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
├── config.py ← variáveis de ambiente
|
||||||
|
├── dynamo.py ← depende de config
|
||||||
|
├── tools.py ← depende de config, dynamo
|
||||||
|
├── agent_bedrock.py ← depende de config, tools
|
||||||
|
└── orquestrador.py ← depende de config, dynamo, agent_bedrock
|
||||||
|
```
|
||||||
|
|
||||||
|
Não há dependências circulares entre os módulos.
|
||||||
|
|
||||||
|
### `config.py`
|
||||||
|
|
||||||
|
Lê todas as variáveis de ambiente na inicialização e exporta como constantes:
|
||||||
|
|
||||||
|
| Variável | Descrição |
|
||||||
|
|----------|-----------|
|
||||||
|
| `TABLE` | Tabela DynamoDB |
|
||||||
|
| `REGION` | Região AWS |
|
||||||
|
| `AWS_ACCOUNT` | ID da conta AWS |
|
||||||
|
| `SECRET_NAME` | Nome do secret no Secrets Manager |
|
||||||
|
|
||||||
|
### `dynamo.py`
|
||||||
|
|
||||||
|
- Instancia o cliente `dynamodb` (boto3) usando `REGION`
|
||||||
|
- `get_secret()` — busca credenciais do Langfuse no Secrets Manager
|
||||||
|
- Inicializa o objeto `langfuse` na carga do módulo
|
||||||
|
- `get_contexto(dashboard: str) -> dict` — carrega contexto, filtro e itens disponíveis do DynamoDB para o dashboard informado
|
||||||
|
|
||||||
|
### `tools.py`
|
||||||
|
|
||||||
|
Define a classe `ReportTools`, instanciada por requisição com o mapeamento de IDs local → real.
|
||||||
|
|
||||||
|
```
|
||||||
|
ReportTools(id_mapping: dict[str, str])
|
||||||
|
├── get_variable_value(id, variable) — busca uma variável no DynamoDB
|
||||||
|
├── get_variables_list(id) — lista as variáveis disponíveis para um ID
|
||||||
|
└── as_tools() -> list[StructuredTool] — retorna as tools prontas para o agente
|
||||||
|
```
|
||||||
|
|
||||||
|
| Tool (nome exposto ao LLM) | Método | Descrição |
|
||||||
|
|---------------------------|--------|-----------|
|
||||||
|
| `get_variable_value` | `get_variable_value(id, variable)` | Busca o valor de uma variável no DynamoDB para o ID informado |
|
||||||
|
| `get_variable_list` | `get_variables_list(id)` | Lista as variáveis disponíveis para o ID informado |
|
||||||
|
|
||||||
|
Os IDs expostos ao LLM são locais (`id_1`, `id_2`, …). Internamente cada método converte o ID local para o ID real do DynamoDB via `self.id_mapping`. Por usar estado de instância em vez de variável global, múltiplas requisições simultâneas ficam completamente isoladas.
|
||||||
|
|
||||||
|
### `agent_bedrock.py`
|
||||||
|
|
||||||
|
- `AgentState` — TypedDict com `messages` e `current_step`
|
||||||
|
- `create_bedrock_llm(model_id, region, tools)` — instancia `ChatBedrockConverse` e vincula as tools via `bind_tools`
|
||||||
|
- `call_model(state, llm)` — nó do grafo: invoca o LLM
|
||||||
|
- `call_tools(state, tools_map)` — nó do grafo: executa as tool calls usando o `tools_map` da requisição
|
||||||
|
- `should_continue(state)` — roteador: `"tools"` se há tool_calls, `"end"` se não
|
||||||
|
- `create_agent(inference_profile_arn, region, tools)` — constrói `tools_map = {t.name: t for t in tools}`, monta e compila o `StateGraph`
|
||||||
|
|
||||||
|
**Fluxo do Grafo:**
|
||||||
|
|
||||||
|
```
|
||||||
|
SystemMessage + HumanMessage
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────┐
|
||||||
|
│ model │ ◄── LLM via Bedrock
|
||||||
|
└────┬────┘
|
||||||
|
│
|
||||||
|
should_continue?
|
||||||
|
├── tool_calls → ┌───────┐
|
||||||
|
│ │ tools │ → executa tools → volta ao model
|
||||||
|
│ └───────┘
|
||||||
|
└── (fim) → [END]
|
||||||
|
```
|
||||||
|
|
||||||
|
### `orquestrador.py`
|
||||||
|
|
||||||
|
Ponto de entrada da lógica de negócio. A função `main(user_query, history, model, base)`:
|
||||||
|
|
||||||
|
1. Chama `get_contexto(base)` para carregar o contexto do dashboard
|
||||||
|
2. Constrói `id_mapping` (`id_1`, `id_2`, … → IDs reais do DynamoDB) e `local_items` (IDs locais → descrições)
|
||||||
|
3. Instancia `ReportTools(id_mapping)` com o mapeamento isolado da requisição
|
||||||
|
4. Monta o `SYSTEM_PROMPT` dinamicamente com contexto, regras de filtro, `local_items` e histórico
|
||||||
|
5. Cria o agente via `create_agent(model, REGION, tools=report_tools.as_tools())`
|
||||||
|
6. Invoca o agente com o estado inicial
|
||||||
|
7. Agrega tokens de todos os `AIMessage` do estado final
|
||||||
|
8. Retorna `response`, `input_tokens`, `output_tokens`, `total_tokens`
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
|
||||||
|
### `front.py` — Streamlit (porta 8501)
|
||||||
|
|
||||||
|
- Importa `from backend import orquestrador`
|
||||||
|
- Lista bases disponíveis consultando DynamoDB (`item_type_index`, `item_type = "contexto"`)
|
||||||
|
- Seleção de modelo LLM e base via `st.selectbox`
|
||||||
|
- Histórico de conversas em `session_state`
|
||||||
|
- Efeito de digitação caractere a caractere
|
||||||
|
- Exibe consumo de tokens por resposta
|
||||||
|
|
||||||
|
### `api.py` — FastAPI (porta 8000)
|
||||||
|
|
||||||
|
- Importa `from .backend import orquestrador`
|
||||||
|
- `GET /` — health check
|
||||||
|
- `POST /agent` — recebe `QueryRequest`, chama `orquestrador.main()`, retorna `QueryResponse`
|
||||||
|
|
||||||
|
## Modelos LLM Suportados
|
||||||
|
|
||||||
|
| Modelo | Provider | Prefixo de Rota |
|
||||||
|
|--------|----------|-----------------|
|
||||||
|
| `anthropic.claude-haiku-4-5-20251001-v1:0` | Anthropic | `us` |
|
||||||
|
| `anthropic.claude-sonnet-4-5-20250929-v1:0` | Anthropic | `global` |
|
||||||
|
| `meta.llama4-maverick-17b-instruct-v1:0` | Meta | `us` |
|
||||||
|
| `meta.llama4-scout-17b-instruct-v1:0` | Meta | `us` |
|
||||||
|
| `amazon.nova-lite-v1:0` | Amazon | `us` |
|
||||||
|
| `amazon.nova-pro-v1:0` | Amazon | `us` |
|
||||||
|
| `amazon.nova-2-lite-v1:0` | Amazon | `global` |
|
||||||
|
|
||||||
|
Todos acessados via AWS Bedrock inference profiles cross-region. O ARN é construído dinamicamente com `REGION` e `AWS_ACCOUNT`.
|
||||||
129
docs/data-model.md
Normal file
129
docs/data-model.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Modelo de Dados
|
||||||
|
|
||||||
|
## Visão Geral
|
||||||
|
|
||||||
|
Os dados de contexto e relatórios pré-processados são armazenados e consumidos diretamente do **DynamoDB**.
|
||||||
|
|
||||||
|
## Estrutura de Tabelas
|
||||||
|
|
||||||
|
Cada combinação cliente + dashboard gera **três tabelas**:
|
||||||
|
|
||||||
|
```
|
||||||
|
{cliente}_{dashboard}_contatos
|
||||||
|
{cliente}_{dashboard}_respostas
|
||||||
|
{cliente}_{dashboard}_pesquisa
|
||||||
|
```
|
||||||
|
|
||||||
|
### Convenção de Nomenclatura
|
||||||
|
|
||||||
|
- Letras minúsculas
|
||||||
|
- Espaços e hífens substituídos por `_`
|
||||||
|
|
||||||
|
**Exemplo:**
|
||||||
|
- Cliente: `Bacio Di Latte`, Dashboard: `Transacional Loja App`
|
||||||
|
- Tabelas: `bacio_transacional_loja_app_contatos`, `bacio_transacional_loja_app_respostas`, `bacio_transacional_loja_app_pesquisa`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. `{cliente}_{dashboard}_contatos`
|
||||||
|
|
||||||
|
Contatos recebidos para ativar as pesquisas. Cada linha = um contato único.
|
||||||
|
|
||||||
|
**Colunas Fixas:**
|
||||||
|
|
||||||
|
| Coluna | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `contact_id` | Identificador único do contato |
|
||||||
|
| `surveyid` | Identificador da pesquisa associada |
|
||||||
|
| `name` | Nome do contato |
|
||||||
|
| `email` | E-mail |
|
||||||
|
| `phone` | Telefone |
|
||||||
|
| `status` | Status (enviado, inválido, abriu e-mail, etc.) |
|
||||||
|
| `url` | Link único de resposta |
|
||||||
|
| `data_recebimento` | Data de recebimento (`yyyy-mm-dd`) |
|
||||||
|
| `mes_recebimento` | Mês de recebimento (`mm/yyyy`) |
|
||||||
|
|
||||||
|
**Colunas Variáveis** (dependem do cliente): `cidade`, `estado`, `cpf`, `produto_comprado`, `canal_de_compra`, `nome_da_loja`, `valor_da_venda`, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `{cliente}_{dashboard}_respostas`
|
||||||
|
|
||||||
|
Respostas consolidadas da pesquisa. Cada linha = um respondente único (versão mais atualizada).
|
||||||
|
|
||||||
|
**Colunas Fixas:**
|
||||||
|
|
||||||
|
| Coluna | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `responseid` | Identificador único da resposta |
|
||||||
|
| `status` | Status (completou, parcial, inválida) |
|
||||||
|
| `contact_id` | Referência ao contato (FK) |
|
||||||
|
| `data_resposta` | Data da resposta (`yyyy-mm-dd`) |
|
||||||
|
| `mes_resposta` | Mês da resposta (`mm/yyyy`) |
|
||||||
|
|
||||||
|
**Colunas Variáveis (Perguntas):** Uma coluna por pergunta, nomeada pelo `shortname` da tabela `pesquisa`. Exemplos: `nps`, `atendimento_cordialidade`, `achou_produto`, `comentarios`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `{cliente}_{dashboard}_pesquisa`
|
||||||
|
|
||||||
|
Catálogo de perguntas dos questionários. Dicionário das colunas dinâmicas da tabela de respostas.
|
||||||
|
|
||||||
|
| Coluna | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `order_by` | Ordem da pergunta no questionário |
|
||||||
|
| `title` | Texto completo da pergunta |
|
||||||
|
| `shortname` | Alias usado como nome de coluna em `respostas` |
|
||||||
|
| `status` | Status (ativa, oculta, inativa) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Relacionamentos
|
||||||
|
|
||||||
|
```
|
||||||
|
contatos (1) ─── (N) respostas (join via contact_id)
|
||||||
|
|
||||||
|
pesquisa ──────────▶ respostas (shortname = nome das colunas dinâmicas)
|
||||||
|
```
|
||||||
|
|
||||||
|
## DynamoDB — Tabela `poc_dnx_monthly_summary`
|
||||||
|
|
||||||
|
Armazena dados pré-processados e contexto para o agente.
|
||||||
|
|
||||||
|
### Estrutura
|
||||||
|
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| `id` | `string` (PK) | Identificador (ex: `1`, `2`, `bacio_transacional_loja_app_contexto`) |
|
||||||
|
| `item_type` | `string` (GSI) | Tipo do item (ex: `contexto`) |
|
||||||
|
| `contexto` | `string` | Descrição contextual do dashboard |
|
||||||
|
| `filter_key` | `string` | Tipo de filtro: `period` ou `event` |
|
||||||
|
| `itens_disponiveis` | `map` | Dicionário `{id: descrição}` dos dados disponíveis |
|
||||||
|
| `chaves_consolidadas` | `string` | Lista de colunas/variáveis disponíveis por ID |
|
||||||
|
| *variáveis dinâmicas* | `string` | Dados pré-processados (resumos mensais, NPS, etc.) |
|
||||||
|
|
||||||
|
### Índice Secundário Global
|
||||||
|
|
||||||
|
- **`item_type_index`** — Permite consultar todos os itens de um tipo (ex: listar todas as bases com `item_type = "contexto"`)
|
||||||
|
|
||||||
|
### Formato do ID de Período
|
||||||
|
|
||||||
|
Quando `filter_key = "period"`, os IDs seguem o formato `year_month`:
|
||||||
|
- Ano: 4 dígitos (2025, 2024, etc.)
|
||||||
|
- Mês: 2 dígitos (01 a 12)
|
||||||
|
- Exemplo: `2025_05` = maio de 2025
|
||||||
|
|
||||||
|
### Formato do ID de Evento
|
||||||
|
|
||||||
|
Quando `filter_key = "event"`, os IDs descrevem o evento:
|
||||||
|
- Formato: `Nome - Cidade DD/MM/YYYY`
|
||||||
|
|
||||||
|
## Cálculo de NPS
|
||||||
|
|
||||||
|
Dentro dos dados existe a variável `NPS` que contém `distribuicao` com notas e quantidade de respondentes:
|
||||||
|
|
||||||
|
- Notas **0-6**: Detratores
|
||||||
|
- Notas **7-8**: Neutros
|
||||||
|
- Notas **9-10**: Promotores
|
||||||
|
|
||||||
|
**Fórmula:** `NPS = %Promotores - %Detratores`
|
||||||
92
docs/deployment.md
Normal file
92
docs/deployment.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Deploy
|
||||||
|
|
||||||
|
## Fluxo
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Build Docker ──▶ 2. Push ECR ──▶ 3. Atualizar SHA no Pulumi ──▶ 4. pulumi up
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Build da Imagem Docker
|
||||||
|
|
||||||
|
Execute a partir da raiz do repositório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build code/ --platform linux/amd64 -t <ECR_REPO_NAME>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Push para o ECR
|
||||||
|
|
||||||
|
### Autenticar no ECR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws ecr get-login-password --region <REGION> \
|
||||||
|
| docker login --username AWS --password-stdin <AWS_ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tag e push
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker tag <ECR_REPO_NAME>:latest \
|
||||||
|
<AWS_ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/<ECR_REPO_NAME>:latest
|
||||||
|
|
||||||
|
docker push <AWS_ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/<ECR_REPO_NAME>:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Obter o SHA da imagem publicada
|
||||||
|
|
||||||
|
Após o push, obtenha o digest da imagem no ECR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws ecr describe-images \
|
||||||
|
--repository-name <ECR_REPO_NAME> \
|
||||||
|
--region <REGION> \
|
||||||
|
--query 'sort_by(imageDetails, &imagePushedAt)[-1].imageDigest' \
|
||||||
|
--output text
|
||||||
|
```
|
||||||
|
|
||||||
|
O retorno será no formato `sha256:xxxxxxxx...`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Atualizar o SHA no Pulumi
|
||||||
|
|
||||||
|
Edite [infra/ecs_alb/Pulumi.Inovyo.yaml](../infra/ecs_alb/Pulumi.Inovyo.yaml) e atualize o campo `ecr_image_digest` com o valor obtido no passo anterior:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ecr_image_digest: sha256:<novo-digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Aplicar com Pulumi
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd infra/ecs_alb
|
||||||
|
pulumi up --stack Inovyo
|
||||||
|
```
|
||||||
|
|
||||||
|
Para visualizar as mudanças antes de aplicar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pulumi preview --diff --stack Inovyo
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuração do Container
|
||||||
|
|
||||||
|
O `entrypoint.sh` inicia dois processos:
|
||||||
|
|
||||||
|
1. **FastAPI** (background): `uvicorn app.api:app --host 0.0.0.0 --port 8000`
|
||||||
|
2. **Streamlit** (foreground): `streamlit run app/front.py --server.port 8501 --server.address 0.0.0.0 --server.headless true`
|
||||||
|
|
||||||
|
Após o deploy, a aplicação fica acessível pelo DNS do ALB:
|
||||||
|
|
||||||
|
- **API:** `http://<alb-dns>:8000`
|
||||||
|
- **Streamlit:** `http://<alb-dns>:8501`
|
||||||
144
docs/development.md
Normal file
144
docs/development.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# Desenvolvimento
|
||||||
|
|
||||||
|
## Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
agente-bd/
|
||||||
|
├── code/ # Código da aplicação
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── api.py # FastAPI — endpoints REST
|
||||||
|
│ │ ├── front.py # Streamlit — interface web
|
||||||
|
│ │ └── backend/
|
||||||
|
│ │ ├── __init__.py # Marca o diretório como pacote Python
|
||||||
|
│ │ ├── config.py # Variáveis de ambiente
|
||||||
|
│ │ ├── dynamo.py # DynamoDB, Langfuse, get_contexto
|
||||||
|
│ │ ├── tools.py # ReportTools: get_variable_value, get_variables_list
|
||||||
|
│ │ ├── agent_bedrock.py # LLM Bedrock, grafo LangGraph
|
||||||
|
│ │ └── orquestrador.py # main() — ponto de entrada
|
||||||
|
│ ├── utils/
|
||||||
|
│ │ └── dynamodb_read_table.py # Utilitários DynamoDB
|
||||||
|
│ ├── main.py # Entry point
|
||||||
|
│ ├── Dockerfile # Imagem Docker
|
||||||
|
│ ├── entrypoint.sh # Script de inicialização
|
||||||
|
│ └── requirements.txt # Dependências Python
|
||||||
|
├── infra/ # Infraestrutura como Código
|
||||||
|
│ ├── ecr/ # Stack ECR
|
||||||
|
│ ├── ecs_alb/ # Stack ECS + ALB
|
||||||
|
│ └── langfuse/ # Stack Langfuse
|
||||||
|
├── docs/ # Esta documentação
|
||||||
|
└── Makefile # Automação de build e deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Responsabilidades dos Módulos do Backend
|
||||||
|
|
||||||
|
### `config.py`
|
||||||
|
|
||||||
|
Lê todas as variáveis de ambiente obrigatórias na carga do módulo e as exporta como constantes. Qualquer módulo que precise de configuração importa daqui.
|
||||||
|
|
||||||
|
### `dynamo.py`
|
||||||
|
|
||||||
|
- Instancia o cliente `dynamodb` e o objeto `langfuse` na carga do módulo
|
||||||
|
- `get_secret()` — busca as credenciais do Langfuse no AWS Secrets Manager
|
||||||
|
- `get_contexto(dashboard: str) -> dict` — retorna `contexto`, `filter` e `items_disponiveis` para o dashboard informado
|
||||||
|
|
||||||
|
### `tools.py`
|
||||||
|
|
||||||
|
Define a classe `ReportTools` com as ferramentas do agente:
|
||||||
|
|
||||||
|
- `ReportTools(id_mapping)` — instanciada por requisição; `id_mapping` converte IDs locais (`id_1`, `id_2`, …) para os IDs reais do DynamoDB
|
||||||
|
- `get_variable_value(id, variable)` — busca o valor de uma variável no DynamoDB
|
||||||
|
- `get_variables_list(id)` — lista as variáveis disponíveis para um ID
|
||||||
|
- `as_tools()` — retorna a lista de `StructuredTool` com nomes `get_variable_value` e `get_variable_list`
|
||||||
|
|
||||||
|
### `agent_bedrock.py`
|
||||||
|
|
||||||
|
- `AgentState` — TypedDict com `messages` e `current_step`
|
||||||
|
- `create_bedrock_llm(model_id, region, tools)` — instancia `ChatBedrockConverse` e vincula as tools
|
||||||
|
- `call_model(state, llm)`, `call_tools(state, tools_map)`, `should_continue(state)` — nós e roteador do grafo LangGraph
|
||||||
|
- `create_agent(inference_profile_arn, region, tools)` — monta o `tools_map` e compila o `StateGraph`
|
||||||
|
|
||||||
|
### `orquestrador.py`
|
||||||
|
|
||||||
|
Função `main(user_query, history, model, base)`:
|
||||||
|
- Carrega contexto via `get_contexto(base)`
|
||||||
|
- Constrói `id_mapping` e `local_items` para isolar IDs reais do LLM
|
||||||
|
- Instancia `ReportTools(id_mapping)` e passa as tools ao agente
|
||||||
|
- Monta o system prompt com `local_items` no lugar dos IDs reais
|
||||||
|
- Cria e invoca o agente
|
||||||
|
- Retorna resposta e contagem de tokens
|
||||||
|
|
||||||
|
## Cadeia de Imports
|
||||||
|
|
||||||
|
```
|
||||||
|
config.py
|
||||||
|
↑
|
||||||
|
dynamo.py ←── config
|
||||||
|
↑
|
||||||
|
tools.py ←── config, dynamo
|
||||||
|
↑
|
||||||
|
agent_bedrock.py ←── config
|
||||||
|
↑
|
||||||
|
orquestrador.py ←── config, dynamo, agent_bedrock, tools
|
||||||
|
↑
|
||||||
|
api.py / front.py ←── orquestrador (via pacote backend)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependências Principais
|
||||||
|
|
||||||
|
| Pacote | Uso |
|
||||||
|
|--------|-----|
|
||||||
|
| `boto3` | SDK AWS (DynamoDB, Secrets Manager) |
|
||||||
|
| `langchain` | Framework de orquestração LLM |
|
||||||
|
| `langchain-aws` | Integração LangChain + AWS Bedrock |
|
||||||
|
| `langgraph` | Framework de agentes baseado em grafos |
|
||||||
|
| `streamlit` | Interface web interativa |
|
||||||
|
| `fastapi` | Framework de API REST |
|
||||||
|
| `uvicorn` | Servidor ASGI |
|
||||||
|
| `langfuse` | Observabilidade e tracing de LLMs |
|
||||||
|
|
||||||
|
## Fluxo de Desenvolvimento
|
||||||
|
|
||||||
|
1. **Fazer alterações** — Editar os arquivos em `code/app/`
|
||||||
|
3. **Testar localmente** — Rodar FastAPI + Streamlit ou via Docker
|
||||||
|
4. **Validar** — Executar scripts de teste em `scripts/`
|
||||||
|
5. **Build e deploy** — Seguir o [guia de deploy](deployment.md)
|
||||||
|
|
||||||
|
## Adicionando uma Nova Ferramenta ao Agente
|
||||||
|
|
||||||
|
Todas as ferramentas vivem na classe `ReportTools` em `tools.py`. Basta:
|
||||||
|
|
||||||
|
1. Adicionar o método à classe:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def minha_nova_tool(self, param: str) -> str:
|
||||||
|
"""Descrição da tool para o LLM."""
|
||||||
|
real_id = self.id_mapping.get(param, param) # se precisar resolver ID
|
||||||
|
# implementação
|
||||||
|
return resultado
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Registrá-lo em `as_tools()`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def as_tools(self) -> list:
|
||||||
|
return [
|
||||||
|
...,
|
||||||
|
StructuredTool.from_function(
|
||||||
|
self.minha_nova_tool,
|
||||||
|
name="minha_nova_tool",
|
||||||
|
description="Descrição da tool para o LLM.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Não é necessário alterar `agent_bedrock.py` — o `tools_map` é construído dinamicamente a partir da lista retornada por `as_tools()`.
|
||||||
|
|
||||||
|
## Adicionando um Novo Modelo LLM
|
||||||
|
|
||||||
|
Em `agent_bedrock.py`, adicionar o novo modelo aos três dicionários em `create_bedrock_llm()`:
|
||||||
|
|
||||||
|
- `MODEL_ARNS` — ARN do inference profile
|
||||||
|
- `PROVIDER` — provider (`anthropic`, `meta`, `amazon`)
|
||||||
|
- `prefix` — prefixo de rota (`us` ou `global`)
|
||||||
|
|
||||||
|
E adicionar o model ID à lista `MODELS` em `front.py`.
|
||||||
126
docs/infrastructure.md
Normal file
126
docs/infrastructure.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Infraestrutura
|
||||||
|
|
||||||
|
A infraestrutura é gerenciada via **Pulumi** (IaC em Python) e provisionada na AWS.
|
||||||
|
|
||||||
|
## Visão Geral dos Recursos
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────┐
|
||||||
|
│ AWS Account 305427701314 │
|
||||||
|
│ (us-east-1) │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────┐ │
|
||||||
|
│ │ Application Load Balancer │ │
|
||||||
|
│ │ Portas: 8501 (Streamlit), 8000 (API) │ │
|
||||||
|
│ │ Subnets: públicas (2) │ │
|
||||||
|
│ └─────────────────┬────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌─────────────────▼────────────────────────┐ │
|
||||||
|
│ │ ECS Fargate Cluster │ │
|
||||||
|
│ │ Task: assistente-analitico-db-dev │ │
|
||||||
|
│ │ CPU: 256 | Memória: 512 MB │ │
|
||||||
|
│ │ Auto-scaling: 1–3 instâncias (60% CPU) │ │
|
||||||
|
│ │ Subnets: privadas (2) │ │
|
||||||
|
│ └──────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||||
|
│ │ ECR │ │ KMS │ │ Secrets │ │
|
||||||
|
│ │ Repo │ │ Key │ │ Manager │ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────────────────────────┐ │
|
||||||
|
│ │ CloudWatch Logs │ │
|
||||||
|
│ │ Log Group: assistente-analitico-db-dev │ │
|
||||||
|
│ │ Retenção: 7 dias │ │
|
||||||
|
│ └──────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stacks Pulumi
|
||||||
|
|
||||||
|
O diretório `infra/` contém três stacks independentes:
|
||||||
|
|
||||||
|
### 1. `infra/ecr/` — Elastic Container Registry
|
||||||
|
|
||||||
|
Cria o repositório ECR para armazenar as imagens Docker.
|
||||||
|
|
||||||
|
| Configuração | Valor |
|
||||||
|
|-------------|-------|
|
||||||
|
| Stack | `inovyo` |
|
||||||
|
| Repositório | `assistente-analitico-db-dev` |
|
||||||
|
|
||||||
|
### 2. `infra/ecs_alb/` — ECS + Application Load Balancer
|
||||||
|
|
||||||
|
Stack principal que provisiona o cluster ECS Fargate com ALB.
|
||||||
|
|
||||||
|
**Configurações principais:**
|
||||||
|
|
||||||
|
| Configuração | Valor |
|
||||||
|
|-------------|-------|
|
||||||
|
| Stack | `Inovyo` |
|
||||||
|
| Projeto | `assistente-analitico` |
|
||||||
|
| Ambiente | `dev` |
|
||||||
|
| Conta AWS | `305427701314` |
|
||||||
|
| Região | `us-east-1` |
|
||||||
|
|
||||||
|
**Rede:**
|
||||||
|
|
||||||
|
| Recurso | Valor |
|
||||||
|
|---------|-------|
|
||||||
|
| VPC | `vpc-17ceb96c` |
|
||||||
|
| Subnets ALB | 2 subnets públicas |
|
||||||
|
| Subnets ECS | 2 subnets privadas |
|
||||||
|
| Ingress CIDR | `3.14.44.224/32` (IP restrito) |
|
||||||
|
| ALB interno | Não (externamente acessível) |
|
||||||
|
|
||||||
|
**ECS Task:**
|
||||||
|
|
||||||
|
| Configuração | Valor |
|
||||||
|
|-------------|-------|
|
||||||
|
| Task Name | `assisnte-analitico-db-dev` |
|
||||||
|
| CPU | 256 |
|
||||||
|
| Memória | 512 MB |
|
||||||
|
| Desired Count | 1 |
|
||||||
|
| Auto-scaling | 1–3 instâncias, target 60% CPU |
|
||||||
|
| Portas | 8000 (API), 8501 (Streamlit) |
|
||||||
|
|
||||||
|
**Variáveis de ambiente do container:**
|
||||||
|
|
||||||
|
| Variável | Descrição |
|
||||||
|
|----------|-----------|
|
||||||
|
| `LANGFUSE_HOST` | Endpoint do Langfuse |
|
||||||
|
|
||||||
|
**Módulos inclusos:**
|
||||||
|
- `iam.py` — Roles de execução e task com políticas para Bedrock, DynamoDB, Secrets Manager, CloudWatch
|
||||||
|
- `ecs.py` — Definição de task, service, target groups, listeners e auto-scaling
|
||||||
|
- `ecr.py` — Referência ao repositório ECR
|
||||||
|
- `kms.py` — Chave KMS para criptografia
|
||||||
|
- `conf.py` — Carregamento de configuração do Pulumi
|
||||||
|
- `autotag/` — Auto-tagging de recursos AWS
|
||||||
|
|
||||||
|
### 3. `infra/langfuse/` — Langfuse
|
||||||
|
|
||||||
|
Provisionamento da infraestrutura para o serviço de observabilidade Langfuse.
|
||||||
|
|
||||||
|
| Configuração | Valor |
|
||||||
|
|-------------|-------|
|
||||||
|
| Stack | `inovyo` |
|
||||||
|
| Host | `http://172.31.252.176:3000` |
|
||||||
|
|
||||||
|
## Serviços AWS Utilizados
|
||||||
|
|
||||||
|
| Serviço | Uso |
|
||||||
|
|---------|-----|
|
||||||
|
| **ECS Fargate** | Orquestração de containers |
|
||||||
|
| **ALB** | Balanceamento de carga |
|
||||||
|
| **ECR** | Registry de imagens Docker |
|
||||||
|
| **Bedrock** | Inferência de modelos LLM |
|
||||||
|
| **DynamoDB** | Contexto e dados pré-processados do agente |
|
||||||
|
| **Secrets Manager** | Credenciais do Langfuse |
|
||||||
|
| **KMS** | Criptografia |
|
||||||
|
| **CloudWatch** | Logs |
|
||||||
|
| **IAM** | Controle de acesso |
|
||||||
|
|
||||||
|
## Como Deployar Infraestrutura
|
||||||
|
|
||||||
|
Consulte o [Guia de Deploy](deployment.md) para instruções detalhadas.
|
||||||
162
docs/langfuse-guide.md
Normal file
162
docs/langfuse-guide.md
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# Guia de Integração Langfuse
|
||||||
|
|
||||||
|
Este guia descreve como o Langfuse está integrado ao assistente e como utilizar suas funcionalidades de observabilidade e rastreamento.
|
||||||
|
|
||||||
|
## Índice
|
||||||
|
|
||||||
|
- [O que é Langfuse](#o-que-e-langfuse)
|
||||||
|
- [Como está integrado](#como-esta-integrado)
|
||||||
|
- [Credenciais](#credenciais)
|
||||||
|
- [Rastreamento automático via LangChain](#rastreamento-automatico-via-langchain)
|
||||||
|
- [Tags por dashboard](#tags-por-dashboard)
|
||||||
|
- [Adicionando scores customizados](#adicionando-scores-customizados)
|
||||||
|
- [Visualizando traces](#visualizando-traces)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O que é Langfuse
|
||||||
|
|
||||||
|
Langfuse é uma plataforma de observabilidade para aplicações LLM que permite:
|
||||||
|
|
||||||
|
- Rastrear automaticamente todas as chamadas ao modelo (tokens, latência, erros)
|
||||||
|
- Inspecionar o histórico de mensagens e chamadas de tools
|
||||||
|
- Adicionar scores e métricas customizadas por trace
|
||||||
|
- Analisar performance e uso ao longo do tempo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como está integrado
|
||||||
|
|
||||||
|
A integração é feita em dois módulos:
|
||||||
|
|
||||||
|
### `dynamo.py` — inicialização do cliente
|
||||||
|
|
||||||
|
O cliente `Langfuse` é instanciado na carga do módulo, usando credenciais obtidas do AWS Secrets Manager:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langfuse import Langfuse
|
||||||
|
|
||||||
|
secrets = json.loads(get_secret())
|
||||||
|
langfuse = Langfuse(
|
||||||
|
public_key=secrets["LANGFUSE-PUBLIC-KEY"],
|
||||||
|
secret_key=secrets["LANGFUSE-SECRET-KEY"],
|
||||||
|
host=os.environ["LANGFUSE_HOST"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
O objeto `langfuse` é exportado e reutilizado pelo `orquestrador.py`.
|
||||||
|
|
||||||
|
### `orquestrador.py` — rastreamento por execução
|
||||||
|
|
||||||
|
A cada chamada ao agente, um `CallbackHandler` do LangChain é criado e passado na configuração do grafo LangGraph. Isso registra automaticamente no Langfuse todas as etapas da execução — chamadas ao modelo, chamadas de tools e mensagens trocadas.
|
||||||
|
|
||||||
|
Ao final da execução, `langfuse.flush()` garante o envio dos dados pendentes.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langfuse.langchain import CallbackHandler
|
||||||
|
from .dynamo import langfuse
|
||||||
|
|
||||||
|
langfuse_handler = CallbackHandler()
|
||||||
|
config = {"callbacks": [langfuse_handler], "tags": [base]}
|
||||||
|
final_state = agent.invoke(initial_state, config=config)
|
||||||
|
|
||||||
|
langfuse.flush()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Credenciais
|
||||||
|
|
||||||
|
As chaves do Langfuse são armazenadas no AWS Secrets Manager, no secret definido pela variável de ambiente `SECRET_NAME`. O secret deve conter as seguintes chaves:
|
||||||
|
|
||||||
|
| Chave no secret | Descrição |
|
||||||
|
|-----------------|-----------|
|
||||||
|
| `LANGFUSE-PUBLIC-KEY` | Chave pública do projeto Langfuse |
|
||||||
|
| `LANGFUSE-SECRET-KEY` | Chave secreta do projeto Langfuse |
|
||||||
|
|
||||||
|
O endpoint do servidor é configurado pela variável de ambiente `LANGFUSE_HOST`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rastreamento automático via LangChain
|
||||||
|
|
||||||
|
O `CallbackHandler` captura automaticamente, sem código adicional:
|
||||||
|
|
||||||
|
- Cada chamada ao modelo Bedrock (input, output, tokens)
|
||||||
|
- Chamadas às tools `get_monthly_report` e `get_consolidated_keys`
|
||||||
|
- Sequência de passos do grafo LangGraph
|
||||||
|
- Erros e exceções durante a execução
|
||||||
|
|
||||||
|
Cada invocação de `orquestrador.main()` gera um trace independente no Langfuse.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tags por dashboard
|
||||||
|
|
||||||
|
A tag `base` (nome do dashboard, ex: `bacio_transacional_loja_app`) é passada em cada execução:
|
||||||
|
|
||||||
|
```python
|
||||||
|
config = {"callbacks": [langfuse_handler], "tags": [base]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso permite filtrar traces no Langfuse por cliente/dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adicionando scores customizados
|
||||||
|
|
||||||
|
Para registrar métricas adicionais em um trace, use a API do cliente `langfuse` após a execução do agente. O trace ID pode ser obtido via `langfuse_handler.get_trace_id()`.
|
||||||
|
|
||||||
|
### Tipos de score
|
||||||
|
|
||||||
|
| Tipo | Uso |
|
||||||
|
|------|-----|
|
||||||
|
| `NUMERIC` | Valores numéricos (ex: satisfação 1–5, tempo de resposta) |
|
||||||
|
| `CATEGORICAL` | Valores de categoria (ex: canal de origem, qualidade) |
|
||||||
|
| `BOOLEAN` | Verdadeiro/falso |
|
||||||
|
|
||||||
|
### Exemplo: score após execução
|
||||||
|
|
||||||
|
```python
|
||||||
|
from langfuse.langchain import CallbackHandler
|
||||||
|
from .dynamo import langfuse
|
||||||
|
|
||||||
|
langfuse_handler = CallbackHandler()
|
||||||
|
config = {"callbacks": [langfuse_handler], "tags": [base]}
|
||||||
|
final_state = agent.invoke(initial_state, config=config)
|
||||||
|
|
||||||
|
trace_id = langfuse_handler.get_trace_id()
|
||||||
|
if trace_id:
|
||||||
|
langfuse.score(
|
||||||
|
trace_id=trace_id,
|
||||||
|
name="canal_origem",
|
||||||
|
value="api",
|
||||||
|
data_type="CATEGORICAL",
|
||||||
|
)
|
||||||
|
|
||||||
|
langfuse.flush()
|
||||||
|
```
|
||||||
|
|
||||||
|
> Sempre chame `langfuse.flush()` depois de registrar scores para garantir o envio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Visualizando traces
|
||||||
|
|
||||||
|
Acesse a interface web do Langfuse no endereço configurado em `LANGFUSE_HOST`.
|
||||||
|
|
||||||
|
### Navegação útil
|
||||||
|
|
||||||
|
| O que ver | Onde ir |
|
||||||
|
|-----------|---------|
|
||||||
|
| Todas as execuções | Traces |
|
||||||
|
| Execuções por dashboard | Traces → filtrar por tag |
|
||||||
|
| Tokens por modelo | Dashboard → Usage |
|
||||||
|
| Erros e falhas | Traces → filtrar por status `ERROR` |
|
||||||
|
| Scores registrados | Trace individual → aba Scores |
|
||||||
|
|
||||||
|
### Referências
|
||||||
|
|
||||||
|
- [Documentação Oficial Langfuse](https://langfuse.com/docs)
|
||||||
|
- [Integração LangChain/LangGraph](https://langfuse.com/docs/integrations/langchain)
|
||||||
|
- [API de Scores](https://langfuse.com/docs/scores/custom)
|
||||||
256
docs/pulumi-guide.md
Normal file
256
docs/pulumi-guide.md
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# Guia Pulumi
|
||||||
|
|
||||||
|
## O que é Pulumi?
|
||||||
|
|
||||||
|
Pulumi é uma ferramenta de **Infrastructure as Code (IaC)**: em vez de clicar no console da AWS, você descreve os recursos em código Python (ou outras linguagens) e o Pulumi cria, atualiza e destrói esses recursos de forma controlada e reproduzível.
|
||||||
|
|
||||||
|
### Conceitos básicos
|
||||||
|
|
||||||
|
| Conceito | O que é |
|
||||||
|
|----------|---------|
|
||||||
|
| **Projeto** | O diretório com `Pulumi.yaml` — define o nome e a linguagem do projeto |
|
||||||
|
| **Stack** | Um ambiente independente do mesmo projeto (ex: `dev`, `prod`, `Inovyo`). Cada stack tem seu próprio estado e configuração |
|
||||||
|
| **Estado** | Arquivo que o Pulumi mantém mapeando cada recurso do código a um recurso real na AWS. É a "memória" do Pulumi |
|
||||||
|
| **`pulumi preview`** | Mostra o que **vai** mudar, sem aplicar nada — equivalente a um "dry run" |
|
||||||
|
| **`pulumi up`** | Aplica as mudanças: cria, atualiza ou remove recursos para corresponder ao código |
|
||||||
|
| **Output** | Valores exportados pelo código após o deploy (ex: a URL do ALB) |
|
||||||
|
|
||||||
|
### Como o Pulumi sabe o que mudar?
|
||||||
|
|
||||||
|
1. Você edita o código ou o arquivo de configuração
|
||||||
|
2. `pulumi preview` compara o código com o estado salvo e mostra o diff
|
||||||
|
3. `pulumi up` aplica o diff na AWS e atualiza o estado
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
- [Pulumi CLI](https://www.pulumi.com/docs/install/) instalado
|
||||||
|
- AWS CLI configurado com credenciais válidas para a conta
|
||||||
|
- Python 3.12+ e `venv`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estrutura do projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
infra/ecs_alb/
|
||||||
|
├── Pulumi.yaml # Metadados do projeto (nome, runtime)
|
||||||
|
├── Pulumi.Inovyo.yaml # Configuração da stack "Inovyo" (região, VPC, ECS, env vars...)
|
||||||
|
├── __main__.py # Ponto de entrada — define ALB, cluster ECS e chama ecs.deploy_app()
|
||||||
|
├── conf.py # Lê as configurações do Pulumi e as expõe como variáveis Python
|
||||||
|
├── ecs.py # Cria task definition, service, target groups, listeners e auto-scaling
|
||||||
|
├── ecr.py # Referencia a imagem no ECR pelo digest ou tag
|
||||||
|
├── iam.py # Cria execution role e task role com políticas (Bedrock, DynamoDB, Secrets Manager...)
|
||||||
|
└── kms.py # Chave KMS (usada opcionalmente para criptografia de segredos)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup inicial
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd infra/ecs_alb
|
||||||
|
|
||||||
|
# Criar e ativar virtualenv
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
# Instalar dependências
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Selecionar a stack
|
||||||
|
pulumi stack select <stack-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comandos principais
|
||||||
|
|
||||||
|
| Comando | O que faz |
|
||||||
|
|---------|-----------|
|
||||||
|
| `pulumi preview` | Mostra o que será criado/alterado/destruído **sem aplicar** |
|
||||||
|
| `pulumi preview --diff` | Igual ao anterior, com diff detalhado de cada recurso |
|
||||||
|
| `pulumi up` | Aplica as mudanças na AWS |
|
||||||
|
| `pulumi up --yes` | Aplica sem pedir confirmação interativa |
|
||||||
|
| `pulumi destroy` | Remove **todos** os recursos da stack da AWS |
|
||||||
|
| `pulumi stack ls` | Lista todas as stacks do projeto |
|
||||||
|
| `pulumi stack output` | Exibe os outputs exportados (ex: URL do ALB) |
|
||||||
|
| `pulumi refresh` | Sincroniza o estado Pulumi com o estado real da AWS |
|
||||||
|
|
||||||
|
Sempre prefira `pulumi preview` antes de `pulumi up` para revisar o impacto das mudanças.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Arquivo de configuração: `Pulumi.Inovyo.yaml`
|
||||||
|
|
||||||
|
Toda a configuração da stack fica nesse arquivo. As seções principais:
|
||||||
|
|
||||||
|
### Rede
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
app-ecs:network:
|
||||||
|
vpc_id: <vpc-id>
|
||||||
|
alb_internal: false
|
||||||
|
alb_subnet_ids: # Subnets públicas do ALB (mínimo 2, mesma região)
|
||||||
|
- <subnet-publica-1>
|
||||||
|
- <subnet-publica-2>
|
||||||
|
alb_allow_ingress_cidr: # IPs que podem acessar o ALB
|
||||||
|
- <ip-permitido>/32
|
||||||
|
ecs_subnet_ids: # Subnets privadas dos containers
|
||||||
|
- <subnet-privada-1>
|
||||||
|
- <subnet-privada-2>
|
||||||
|
```
|
||||||
|
|
||||||
|
### ECS / Container
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
app-ecs:ecs:
|
||||||
|
- task_name: assisnte-analitico-db-dev
|
||||||
|
ecr_repo_name: assistente-analitico-db-dev
|
||||||
|
# Use ecr_image_digest OU ecr_image_tag (nunca os dois)
|
||||||
|
ecr_image_digest: sha256:<digest>
|
||||||
|
cpu: 256
|
||||||
|
memory: 512
|
||||||
|
desired_count: 1
|
||||||
|
use_load_balancer: true
|
||||||
|
auto_scaling:
|
||||||
|
min_capacity: 1
|
||||||
|
max_capacity: 3
|
||||||
|
target_value: 60.0 # % de CPU alvo para escalar
|
||||||
|
lb_configs:
|
||||||
|
- name: api
|
||||||
|
listener_port: 8000
|
||||||
|
target_port: 8000
|
||||||
|
container_port: 8000
|
||||||
|
- name: streamlit
|
||||||
|
listener_port: 8501
|
||||||
|
target_port: 8501
|
||||||
|
container_port: 8501
|
||||||
|
env_variables:
|
||||||
|
TABLE: poc_dnx_monthly_summary
|
||||||
|
REGION: us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fluxo de deploy de nova imagem
|
||||||
|
|
||||||
|
Após buildar e publicar uma nova imagem Docker (veja [deployment.md](deployment.md)):
|
||||||
|
|
||||||
|
**1. Obter o digest da nova imagem:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
aws ecr describe-images \
|
||||||
|
--repository-name assistente-analitico-db-dev \
|
||||||
|
--region us-east-1 \
|
||||||
|
--query 'sort_by(imageDetails, &imagePushedAt)[-1].imageDigest' \
|
||||||
|
--output text
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Atualizar o `Pulumi.Inovyo.yaml`:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ecr_image_digest: sha256:<novo-digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Revisar e aplicar:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd infra/ecs_alb
|
||||||
|
pulumi preview --diff --stack <stack-name>
|
||||||
|
pulumi up --stack <stack-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adicionar ou alterar variáveis de ambiente do container
|
||||||
|
|
||||||
|
Edite a seção `env_variables` em `Pulumi.Inovyo.yaml` e rode `pulumi up`. O Pulumi atualizará a task definition e forçará o re-deploy do serviço ECS automaticamente.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
env_variables:
|
||||||
|
NOVA_VARIAVEL: valor
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verificar o output após o deploy
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pulumi stack output --stack <stack-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Retorna a URL do ALB, por exemplo:
|
||||||
|
```
|
||||||
|
url: http://alb-assistente-analitico-xxxxxxxx.us-east-1.elb.amazonaws.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alterar capacidade de auto-scaling
|
||||||
|
|
||||||
|
Edite `auto_scaling` em `Pulumi.Inovyo.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auto_scaling:
|
||||||
|
min_capacity: 1 # mínimo de tasks rodando
|
||||||
|
max_capacity: 5 # máximo de tasks
|
||||||
|
target_value: 70.0 # escala quando CPU média ultrapassar 70%
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois rode `pulumi up`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permitir acesso ao ALB por Security Group
|
||||||
|
|
||||||
|
Por padrão, o acesso ao ALB é restrito por CIDR (`alb_allow_ingress_cidr`). Para permitir um security group no lugar de um IP, são necessárias duas alterações:
|
||||||
|
|
||||||
|
**1. Edite `__main__.py`** — troque `cidr_blocks` por `security_groups` na regra de ingress do ALB:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ingress=[aws.ec2.SecurityGroupIngressArgs(
|
||||||
|
protocol="-1",
|
||||||
|
from_port=0,
|
||||||
|
to_port=0,
|
||||||
|
security_groups=config.network["alb_allow_ingress_sgs"],
|
||||||
|
)],
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Edite `Pulumi.Inovyo.yaml`** — substitua `alb_allow_ingress_cidr` por:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
app-ecs:network:
|
||||||
|
alb_allow_ingress_sgs:
|
||||||
|
- <security-group-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
> Para manter ambos (IP e SG ao mesmo tempo), adicione os dois campos à mesma `SecurityGroupIngressArgs`, ou crie entradas separadas em `ingress`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Solução de problemas
|
||||||
|
|
||||||
|
**`pulumi up` falha com erro de estado inconsistente:**
|
||||||
|
```bash
|
||||||
|
pulumi refresh --stack <stack-name> # sincroniza estado com AWS
|
||||||
|
pulumi up --stack <stack-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recurso preso em estado de update:**
|
||||||
|
```bash
|
||||||
|
pulumi stack export --stack <stack-name> > state.json
|
||||||
|
# Editar state.json para remover o recurso problemático
|
||||||
|
pulumi stack import --stack <stack-name> < state.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ver logs do container após o deploy:**
|
||||||
|
```bash
|
||||||
|
aws logs tail <nome-do-log-group> --follow --region us-east-1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verificar tasks rodando no ECS:**
|
||||||
|
```bash
|
||||||
|
aws ecs list-tasks --cluster <nome-do-cluster> --region us-east-1
|
||||||
|
```
|
||||||
10
infra/ecr/.gitignore
vendored
Normal file
10
infra/ecr/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
@@ -26,7 +26,7 @@ config:
|
|||||||
- task_name: assisnte-analitico-db-dev
|
- task_name: assisnte-analitico-db-dev
|
||||||
ecr_repo_name: assistente-analitico-db-dev
|
ecr_repo_name: assistente-analitico-db-dev
|
||||||
ecr_image_tag: latest
|
ecr_image_tag: latest
|
||||||
ecr_image_digest: sha256:c9cd83a7caf51e7eee471d47649bbe0157d4c2703f365010f12997e63d941d3d
|
ecr_image_digest: sha256:0bd3a927df4367ba29dbd173e0414d884e973c37599a3f6241341e8d190e827b
|
||||||
cpu: 256
|
cpu: 256
|
||||||
memory: 512
|
memory: 512
|
||||||
desired_count: 1
|
desired_count: 1
|
||||||
@@ -47,6 +47,10 @@ config:
|
|||||||
container_port: 8000
|
container_port: 8000
|
||||||
env_variables:
|
env_variables:
|
||||||
LANGFUSE_HOST: http://172.31.252.176:3000
|
LANGFUSE_HOST: http://172.31.252.176:3000
|
||||||
|
TABLE: poc_dnx_monthly_summary
|
||||||
|
REGION: us-east-1
|
||||||
|
AWS_ACCOUNT: "305427701314"
|
||||||
|
SECRET_NAME: assistente-db-secrets-manager
|
||||||
# SECRET_NAME: dev/ai-pge-doc-classification
|
# SECRET_NAME: dev/ai-pge-doc-classification
|
||||||
# BEDROCK_REGION: us-east-1
|
# BEDROCK_REGION: us-east-1
|
||||||
# LANGCHAIN_TRACING_V2: "true"
|
# LANGCHAIN_TRACING_V2: "true"
|
||||||
|
|||||||
549
scripts/AthenaToolsSuggestions.py
Normal file
549
scripts/AthenaToolsSuggestions.py
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
"""
|
||||||
|
Additional Tool Suggestions for AWS Athena LangGraph Agent
|
||||||
|
|
||||||
|
This file contains suggested tools that can be added to your LangGraph agent
|
||||||
|
to enhance its capabilities with AWS Athena queries and data analysis.
|
||||||
|
|
||||||
|
Based on: BDAgent.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import time
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# CONFIGURATION
|
||||||
|
# ==============================================
|
||||||
|
WORKGROUP = "iceberg-workgroup"
|
||||||
|
DATABASE = "dnx_warehouse"
|
||||||
|
|
||||||
|
# Initialize Athena client (reuse from your main file)
|
||||||
|
session = boto3.Session()
|
||||||
|
athena = session.client("athena", region_name="us-east-1")
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# HELPER FUNCTION
|
||||||
|
# ==============================================
|
||||||
|
def execute_athena_query(query: str, database: str = DATABASE, workgroup: str = WORKGROUP) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Helper function to execute Athena queries and wait for results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: SQL query string
|
||||||
|
database: Database name
|
||||||
|
workgroup: Athena workgroup
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with query results or error information
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print(f"Executing Athena query...")
|
||||||
|
response = athena.start_query_execution(
|
||||||
|
QueryString=query,
|
||||||
|
QueryExecutionContext={"Database": database},
|
||||||
|
WorkGroup=workgroup
|
||||||
|
)
|
||||||
|
|
||||||
|
query_execution_id = response["QueryExecutionId"]
|
||||||
|
print(f"QueryExecutionId: {query_execution_id}")
|
||||||
|
|
||||||
|
# Wait for query completion
|
||||||
|
while True:
|
||||||
|
result = athena.get_query_execution(QueryExecutionId=query_execution_id)
|
||||||
|
state = result["QueryExecution"]["Status"]["State"]
|
||||||
|
|
||||||
|
if state in ["SUCCEEDED", "FAILED", "CANCELLED"]:
|
||||||
|
print(f"Query state: {state}")
|
||||||
|
break
|
||||||
|
|
||||||
|
print("Waiting for query execution...")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if state == "SUCCEEDED":
|
||||||
|
output = athena.get_query_results(QueryExecutionId=query_execution_id)
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"results": output["ResultSet"]["Rows"],
|
||||||
|
"query_execution_id": query_execution_id
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
error_message = result["QueryExecution"]["Status"].get("StateChangeReason", "Unknown error")
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"error": error_message,
|
||||||
|
"query_execution_id": query_execution_id
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# SUGGESTED TOOLS
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_table_schema(table_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Gets the schema (column names and types) of a specific table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table to describe
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Schema information including column names and data types
|
||||||
|
"""
|
||||||
|
query = f"DESCRIBE {DATABASE}.{table_name};"
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] get_table_schema for {table_name}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error getting schema: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def list_available_tables() -> str:
|
||||||
|
"""
|
||||||
|
Lists all available tables in the database.
|
||||||
|
Useful when user wants to know what data is available.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all tables in the database
|
||||||
|
"""
|
||||||
|
query = f"SHOW TABLES IN {DATABASE};"
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] list_available_tables")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error listing tables: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def count_table_rows(table_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Counts the total number of rows in a specific table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table to count rows from
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Total number of rows in the table
|
||||||
|
"""
|
||||||
|
query = f"SELECT COUNT(*) as total_rows FROM {DATABASE}.{table_name};"
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] count_table_rows for {table_name}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error counting rows: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_column_statistics(table_name: str, column_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Gets statistical information about a numeric column (min, max, avg, count).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table
|
||||||
|
column_name: Name of the numeric column to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Statistical summary of the column
|
||||||
|
"""
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
MIN({column_name}) as min_value,
|
||||||
|
MAX({column_name}) as max_value,
|
||||||
|
AVG({column_name}) as avg_value,
|
||||||
|
COUNT({column_name}) as count_values,
|
||||||
|
COUNT(DISTINCT {column_name}) as distinct_values
|
||||||
|
FROM {DATABASE}.{table_name};
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] get_column_statistics for {table_name}.{column_name}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error getting statistics: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_distinct_values(table_name: str, column_name: str, limit: int = 100) -> str:
|
||||||
|
"""
|
||||||
|
Gets distinct values from a specific column, useful for categorical data analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table
|
||||||
|
column_name: Name of the column to get distinct values from
|
||||||
|
limit: Maximum number of distinct values to return (default: 100)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of distinct values in the column
|
||||||
|
"""
|
||||||
|
query = f"""
|
||||||
|
SELECT DISTINCT {column_name}, COUNT(*) as frequency
|
||||||
|
FROM {DATABASE}.{table_name}
|
||||||
|
GROUP BY {column_name}
|
||||||
|
ORDER BY frequency DESC
|
||||||
|
LIMIT {limit};
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] get_distinct_values for {table_name}.{column_name}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error getting distinct values: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def filter_answers_by_condition(table_name: str, column_name: str, condition: str) -> str:
|
||||||
|
"""
|
||||||
|
Filters answers based on a specific condition.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table (e.g., 'survey_name_respostas')
|
||||||
|
column_name: Column to apply the filter on
|
||||||
|
condition: SQL condition (e.g., "> 5", "= 'Yes'", "IS NOT NULL")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Filtered results matching the condition
|
||||||
|
"""
|
||||||
|
query = f"""
|
||||||
|
SELECT *
|
||||||
|
FROM {DATABASE}.{table_name}
|
||||||
|
WHERE {column_name} {condition}
|
||||||
|
LIMIT 1000;
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] filter_answers_by_condition")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error filtering data: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def aggregate_survey_responses(survey_name: str, column_name: str, aggregation: str = "COUNT") -> str:
|
||||||
|
"""
|
||||||
|
Performs aggregation on survey responses (COUNT, SUM, AVG, etc.).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
survey_name: Name of the survey (will query survey_name_respostas table)
|
||||||
|
column_name: Column to aggregate
|
||||||
|
aggregation: Type of aggregation (COUNT, SUM, AVG, MIN, MAX)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Aggregated result
|
||||||
|
"""
|
||||||
|
table_name = f"{survey_name}_respostas"
|
||||||
|
query = f"""
|
||||||
|
SELECT {aggregation}({column_name}) as result
|
||||||
|
FROM {DATABASE}.{table_name};
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] aggregate_survey_responses - {aggregation} on {column_name}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error performing aggregation: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def group_by_analysis(table_name: str, group_column: str, agg_column: str, agg_function: str = "COUNT") -> str:
|
||||||
|
"""
|
||||||
|
Performs GROUP BY analysis to understand distribution of responses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table
|
||||||
|
group_column: Column to group by
|
||||||
|
agg_column: Column to aggregate (use '*' for COUNT(*))
|
||||||
|
agg_function: Aggregation function (COUNT, SUM, AVG, MIN, MAX)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Grouped analysis results
|
||||||
|
"""
|
||||||
|
if agg_column == '*':
|
||||||
|
agg_expr = f"{agg_function}(*)"
|
||||||
|
else:
|
||||||
|
agg_expr = f"{agg_function}({agg_column})"
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
{group_column},
|
||||||
|
{agg_expr} as aggregated_value
|
||||||
|
FROM {DATABASE}.{table_name}
|
||||||
|
GROUP BY {group_column}
|
||||||
|
ORDER BY aggregated_value DESC
|
||||||
|
LIMIT 100;
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] group_by_analysis")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error performing group by analysis: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def calculate_percentage_distribution(table_name: str, column_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Calculates percentage distribution of values in a column.
|
||||||
|
Useful for understanding response distributions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table
|
||||||
|
column_name: Column to analyze
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Percentage distribution of values
|
||||||
|
"""
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
{column_name},
|
||||||
|
COUNT(*) as count,
|
||||||
|
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2) as percentage
|
||||||
|
FROM {DATABASE}.{table_name}
|
||||||
|
GROUP BY {column_name}
|
||||||
|
ORDER BY count DESC;
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] calculate_percentage_distribution for {column_name}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error calculating distribution: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def compare_columns(table_name: str, column1: str, column2: str) -> str:
|
||||||
|
"""
|
||||||
|
Compares two columns to find correlations or patterns in survey responses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table
|
||||||
|
column1: First column to compare
|
||||||
|
column2: Second column to compare
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cross-tabulation of the two columns
|
||||||
|
"""
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
{column1},
|
||||||
|
{column2},
|
||||||
|
COUNT(*) as frequency
|
||||||
|
FROM {DATABASE}.{table_name}
|
||||||
|
GROUP BY {column1}, {column2}
|
||||||
|
ORDER BY frequency DESC
|
||||||
|
LIMIT 100;
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] compare_columns: {column1} vs {column2}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error comparing columns: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def search_text_in_responses(table_name: str, column_name: str, search_term: str) -> str:
|
||||||
|
"""
|
||||||
|
Searches for specific text in text-based response columns.
|
||||||
|
Useful for open-ended survey questions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table
|
||||||
|
column_name: Column to search in
|
||||||
|
search_term: Text to search for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Matching responses
|
||||||
|
"""
|
||||||
|
query = f"""
|
||||||
|
SELECT {column_name}, COUNT(*) as occurrences
|
||||||
|
FROM {DATABASE}.{table_name}
|
||||||
|
WHERE LOWER({column_name}) LIKE LOWER('%{search_term}%')
|
||||||
|
GROUP BY {column_name}
|
||||||
|
LIMIT 100;
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] search_text_in_responses for '{search_term}'")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error searching text: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_null_count(table_name: str, column_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Counts NULL or missing values in a specific column.
|
||||||
|
Useful for data quality analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table
|
||||||
|
column_name: Column to check for NULL values
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Count of NULL values and percentage of total
|
||||||
|
"""
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_rows,
|
||||||
|
COUNT({column_name}) as non_null_count,
|
||||||
|
COUNT(*) - COUNT({column_name}) as null_count,
|
||||||
|
ROUND((COUNT(*) - COUNT({column_name})) * 100.0 / COUNT(*), 2) as null_percentage
|
||||||
|
FROM {DATABASE}.{table_name};
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] get_null_count for {table_name}.{column_name}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error checking null values: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def get_date_range_data(table_name: str, date_column: str, start_date: str, end_date: str) -> str:
|
||||||
|
"""
|
||||||
|
Retrieves data within a specific date range.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the table
|
||||||
|
date_column: Name of the date column
|
||||||
|
start_date: Start date in format 'YYYY-MM-DD'
|
||||||
|
end_date: End date in format 'YYYY-MM-DD'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Data within the specified date range
|
||||||
|
"""
|
||||||
|
query = f"""
|
||||||
|
SELECT *
|
||||||
|
FROM {DATABASE}.{table_name}
|
||||||
|
WHERE {date_column} BETWEEN DATE '{start_date}' AND DATE '{end_date}'
|
||||||
|
LIMIT 1000;
|
||||||
|
"""
|
||||||
|
result = execute_athena_query(query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] get_date_range_data from {start_date} to {end_date}")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error getting date range data: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def execute_custom_query(sql_query: str) -> str:
|
||||||
|
"""
|
||||||
|
Executes a custom SQL query. USE WITH CAUTION.
|
||||||
|
Only use this when other specific tools don't meet the requirement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sql_query: Complete SQL query to execute
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Query results
|
||||||
|
"""
|
||||||
|
# Add basic validation
|
||||||
|
if any(keyword in sql_query.upper() for keyword in ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE"]):
|
||||||
|
return "Error: Write operations are not allowed. Only SELECT queries are permitted."
|
||||||
|
|
||||||
|
result = execute_athena_query(sql_query)
|
||||||
|
|
||||||
|
if result["status"] == "success":
|
||||||
|
print(f"\n🔧 [TOOL CALLED] execute_custom_query")
|
||||||
|
return result["results"]
|
||||||
|
else:
|
||||||
|
return f"Error executing query: {result.get('error', 'Unknown error')}"
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# TOOL MAPPING FOR EASY INTEGRATION
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
SUGGESTED_TOOLS = [
|
||||||
|
get_table_schema,
|
||||||
|
list_available_tables,
|
||||||
|
count_table_rows,
|
||||||
|
get_column_statistics,
|
||||||
|
get_distinct_values,
|
||||||
|
filter_answers_by_condition,
|
||||||
|
aggregate_survey_responses,
|
||||||
|
group_by_analysis,
|
||||||
|
calculate_percentage_distribution,
|
||||||
|
compare_columns,
|
||||||
|
search_text_in_responses,
|
||||||
|
get_null_count,
|
||||||
|
get_date_range_data,
|
||||||
|
execute_custom_query
|
||||||
|
]
|
||||||
|
|
||||||
|
TOOLS_MAP = {
|
||||||
|
"get_table_schema": get_table_schema,
|
||||||
|
"list_available_tables": list_available_tables,
|
||||||
|
"count_table_rows": count_table_rows,
|
||||||
|
"get_column_statistics": get_column_statistics,
|
||||||
|
"get_distinct_values": get_distinct_values,
|
||||||
|
"filter_answers_by_condition": filter_answers_by_condition,
|
||||||
|
"aggregate_survey_responses": aggregate_survey_responses,
|
||||||
|
"group_by_analysis": group_by_analysis,
|
||||||
|
"calculate_percentage_distribution": calculate_percentage_distribution,
|
||||||
|
"compare_columns": compare_columns,
|
||||||
|
"search_text_in_responses": search_text_in_responses,
|
||||||
|
"get_null_count": get_null_count,
|
||||||
|
"get_date_range_data": get_date_range_data,
|
||||||
|
"execute_custom_query": execute_custom_query
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# INTEGRATION EXAMPLE
|
||||||
|
# ==============================================
|
||||||
|
"""
|
||||||
|
To integrate these tools into your BDAgent.py:
|
||||||
|
|
||||||
|
1. Import the tools:
|
||||||
|
from app.utils.AthenaToolsSuggestions import SUGGESTED_TOOLS, TOOLS_MAP
|
||||||
|
|
||||||
|
2. Update the create_bedrock_llm function:
|
||||||
|
tools = [consult_answers] + SUGGESTED_TOOLS
|
||||||
|
llm_with_tools = llm.bind_tools(tools)
|
||||||
|
|
||||||
|
3. Update the call_tools function:
|
||||||
|
tools_map = {
|
||||||
|
"consult_answers": consult_answers,
|
||||||
|
**TOOLS_MAP # Add all suggested tools
|
||||||
|
}
|
||||||
|
|
||||||
|
4. Update the system prompt to include descriptions of new tools:
|
||||||
|
Your available tools:
|
||||||
|
- consult_answers: Get answers for a specific question by shortname
|
||||||
|
- get_table_schema: Get column names and types of a table
|
||||||
|
- list_available_tables: List all available tables
|
||||||
|
- count_table_rows: Count total rows in a table
|
||||||
|
- get_column_statistics: Get statistics (min, max, avg) for numeric columns
|
||||||
|
- get_distinct_values: Get unique values and frequencies from a column
|
||||||
|
- calculate_percentage_distribution: Calculate percentage distribution of values
|
||||||
|
- group_by_analysis: Perform GROUP BY analysis on data
|
||||||
|
- compare_columns: Compare two columns to find patterns
|
||||||
|
- And more...
|
||||||
|
"""
|
||||||
241
scripts/athena_query_paginated.py
Normal file
241
scripts/athena_query_paginated.py
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
AWS Athena Query Script with Pagination
|
||||||
|
|
||||||
|
This script executes an Athena query and retrieves all results using pagination
|
||||||
|
to overcome the 1000 row limit per API call.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import boto3
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class AthenaQueryExecutor:
|
||||||
|
"""Execute Athena queries with automatic pagination and result retrieval."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
database: str,
|
||||||
|
output_location: str,
|
||||||
|
region_name: str = 'us-east-1',
|
||||||
|
max_results_per_page: int = 1000
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the Athena query executor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
database: The Athena database name
|
||||||
|
output_location: S3 location for query results (e.g., 's3://bucket/path/')
|
||||||
|
region_name: AWS region name
|
||||||
|
max_results_per_page: Maximum results per API call (max 1000)
|
||||||
|
"""
|
||||||
|
self.client = boto3.client('athena', region_name=region_name)
|
||||||
|
self.database = database
|
||||||
|
self.output_location = output_location
|
||||||
|
self.max_results_per_page = min(max_results_per_page, 1000)
|
||||||
|
|
||||||
|
def execute_query(self, query: str, wait: bool = True) -> str:
|
||||||
|
"""
|
||||||
|
Execute an Athena query and return the query execution ID.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: SQL query string
|
||||||
|
wait: Whether to wait for query completion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Query execution ID
|
||||||
|
"""
|
||||||
|
response = self.client.start_query_execution(
|
||||||
|
QueryString=query,
|
||||||
|
QueryExecutionContext={'Database': self.database},
|
||||||
|
ResultConfiguration={'OutputLocation': self.output_location}
|
||||||
|
)
|
||||||
|
|
||||||
|
query_execution_id = response['QueryExecutionId']
|
||||||
|
print(f"Query submitted. Execution ID: {query_execution_id}")
|
||||||
|
|
||||||
|
if wait:
|
||||||
|
self._wait_for_query_completion(query_execution_id)
|
||||||
|
|
||||||
|
return query_execution_id
|
||||||
|
|
||||||
|
def _wait_for_query_completion(self, query_execution_id: str, poll_interval: int = 2):
|
||||||
|
"""
|
||||||
|
Wait for query to complete execution.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query_execution_id: The query execution ID
|
||||||
|
poll_interval: Seconds between status checks
|
||||||
|
"""
|
||||||
|
print("Waiting for query to complete...")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
response = self.client.get_query_execution(
|
||||||
|
QueryExecutionId=query_execution_id
|
||||||
|
)
|
||||||
|
|
||||||
|
state = response['QueryExecution']['Status']['State']
|
||||||
|
|
||||||
|
if state == 'SUCCEEDED':
|
||||||
|
print("Query completed successfully!")
|
||||||
|
stats = response['QueryExecution']['Statistics']
|
||||||
|
print(f"Data scanned: {stats.get('DataScannedInBytes', 0) / (1024**3):.2f} GB")
|
||||||
|
print(f"Execution time: {stats.get('EngineExecutionTimeInMillis', 0) / 1000:.2f} seconds")
|
||||||
|
break
|
||||||
|
elif state in ['FAILED', 'CANCELLED']:
|
||||||
|
reason = response['QueryExecution']['Status'].get('StateChangeReason', 'Unknown')
|
||||||
|
raise Exception(f"Query {state.lower()}: {reason}")
|
||||||
|
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
|
||||||
|
def get_all_results(self, query_execution_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Retrieve all query results using pagination.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query_execution_id: The query execution ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of result rows as dictionaries
|
||||||
|
"""
|
||||||
|
all_results = []
|
||||||
|
next_token = None
|
||||||
|
page_count = 0
|
||||||
|
|
||||||
|
print("Fetching results with pagination...")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
page_count += 1
|
||||||
|
|
||||||
|
# Build request parameters
|
||||||
|
params = {
|
||||||
|
'QueryExecutionId': query_execution_id,
|
||||||
|
'MaxResults': self.max_results_per_page
|
||||||
|
}
|
||||||
|
|
||||||
|
if next_token:
|
||||||
|
params['NextToken'] = next_token
|
||||||
|
|
||||||
|
# Get results page
|
||||||
|
response = self.client.get_query_results(**params)
|
||||||
|
|
||||||
|
# Extract column names from first page
|
||||||
|
if page_count == 1:
|
||||||
|
columns = [col['Name'] for col in response['ResultSet']['ResultSetMetadata']['ColumnInfo']]
|
||||||
|
# Skip header row in first page
|
||||||
|
rows = response['ResultSet']['Rows'][1:]
|
||||||
|
else:
|
||||||
|
rows = response['ResultSet']['Rows']
|
||||||
|
|
||||||
|
# Convert rows to dictionaries
|
||||||
|
for row in rows:
|
||||||
|
values = [field.get('VarCharValue', '') for field in row['Data']]
|
||||||
|
all_results.append(dict(zip(columns, values)))
|
||||||
|
|
||||||
|
print(f"Page {page_count}: Retrieved {len(rows)} rows (Total: {len(all_results)})")
|
||||||
|
|
||||||
|
# Check if there are more results
|
||||||
|
next_token = response.get('NextToken')
|
||||||
|
if not next_token:
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"\nTotal rows retrieved: {len(all_results)}")
|
||||||
|
return all_results
|
||||||
|
|
||||||
|
def query_and_fetch_all(self, query: str) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Execute query and fetch all results in one call.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: SQL query string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of result rows as dictionaries
|
||||||
|
"""
|
||||||
|
query_execution_id = self.execute_query(query, wait=True)
|
||||||
|
return self.get_all_results(query_execution_id)
|
||||||
|
|
||||||
|
def export_to_csv(self, results: List[Dict[str, Any]], filename: str):
|
||||||
|
"""
|
||||||
|
Export results to CSV file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: List of result dictionaries
|
||||||
|
filename: Output CSV filename
|
||||||
|
"""
|
||||||
|
if not results:
|
||||||
|
print("No results to export")
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(filename, 'w', newline='', encoding='utf-8') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=results[0].keys())
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(results)
|
||||||
|
|
||||||
|
print(f"Results exported to {filename}")
|
||||||
|
|
||||||
|
def export_to_json(self, results: List[Dict[str, Any]], filename: str):
|
||||||
|
"""
|
||||||
|
Export results to JSON file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: List of result dictionaries
|
||||||
|
filename: Output JSON filename
|
||||||
|
"""
|
||||||
|
with open(filename, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(results, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
print(f"Results exported to {filename}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Example usage of the AthenaQueryExecutor."""
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DATABASE = 'your_database_name'
|
||||||
|
OUTPUT_LOCATION = 's3://your-bucket/athena-results/'
|
||||||
|
REGION = 'us-east-1'
|
||||||
|
|
||||||
|
# Example query
|
||||||
|
QUERY = """
|
||||||
|
SELECT *
|
||||||
|
FROM your_table
|
||||||
|
WHERE date >= '2024-01-01'
|
||||||
|
LIMIT 5000
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Initialize executor
|
||||||
|
executor = AthenaQueryExecutor(
|
||||||
|
database=DATABASE,
|
||||||
|
output_location=OUTPUT_LOCATION,
|
||||||
|
region_name=REGION
|
||||||
|
)
|
||||||
|
|
||||||
|
# Execute query and fetch all results
|
||||||
|
try:
|
||||||
|
results = executor.query_and_fetch_all(QUERY)
|
||||||
|
|
||||||
|
# Export results
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
executor.export_to_csv(results, f'athena_results_{timestamp}.csv')
|
||||||
|
executor.export_to_json(results, f'athena_results_{timestamp}.json')
|
||||||
|
|
||||||
|
# Display sample results
|
||||||
|
if results:
|
||||||
|
print("\nFirst 5 results:")
|
||||||
|
for i, row in enumerate(results[:5], 1):
|
||||||
|
print(f"{i}. {row}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(main())
|
||||||
191
scripts/dynamodb_read_table.py
Normal file
191
scripts/dynamodb_read_table.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
"""
|
||||||
|
DynamoDB Table Reader Script
|
||||||
|
|
||||||
|
This script connects to AWS DynamoDB and reads all entries from a specified table.
|
||||||
|
Outputs data in XML format with <period> tags containing the context XML content.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from dynamodb_read_table import read_table_as_xml
|
||||||
|
xml_content = read_table_as_xml("my-table-name")
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import boto3
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
|
||||||
|
|
||||||
|
def clean_context_xml(context: str) -> str:
|
||||||
|
"""
|
||||||
|
Remove XML declaration and <relatorio> tags from context content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: Raw XML content from DynamoDB
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Cleaned XML content without declaration and relatorio tags
|
||||||
|
"""
|
||||||
|
# Remove XML declaration (e.g., <?xml version="1.0" encoding="UTF-8"?>)
|
||||||
|
context = re.sub(r'<\?xml[^?]*\?>\s*', '', context)
|
||||||
|
|
||||||
|
# Remove opening <relatorio> tag (with any attributes)
|
||||||
|
context = re.sub(r'<relatorio[^>]*>\s*', '', context)
|
||||||
|
|
||||||
|
# Remove closing </relatorio> tag
|
||||||
|
context = re.sub(r'\s*</relatorio>', '', context)
|
||||||
|
|
||||||
|
return context.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def format_items_to_xml(items: list) -> str:
|
||||||
|
"""
|
||||||
|
Format all DynamoDB items to XML format.
|
||||||
|
|
||||||
|
Each item's 'period' field becomes a <period> tag,
|
||||||
|
and the 'context' field's cleaned XML content is placed inside it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
items: List of DynamoDB items
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete XML formatted string with all items
|
||||||
|
"""
|
||||||
|
xml_parts = []
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
period = item.get("period", "unknown")
|
||||||
|
context = item.get("context", "")
|
||||||
|
|
||||||
|
# Clean the context XML
|
||||||
|
cleaned_context = clean_context_xml(context)
|
||||||
|
|
||||||
|
xml_parts.append(f"<{period}>")
|
||||||
|
xml_parts.append(cleaned_context)
|
||||||
|
xml_parts.append(f"</{period}>")
|
||||||
|
xml_parts.append("") # Empty line between entries
|
||||||
|
|
||||||
|
return "\n".join(xml_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dynamodb_client(region_name: str = "us-east-1"):
|
||||||
|
"""Create and return a DynamoDB client."""
|
||||||
|
session = boto3.Session()
|
||||||
|
return session.client("dynamodb", region_name=region_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dynamodb_resource(region_name: str = "us-east-1"):
|
||||||
|
"""Create and return a DynamoDB resource for higher-level operations."""
|
||||||
|
session = boto3.Session()
|
||||||
|
return session.resource("dynamodb", region_name=region_name)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_table(table_name: str, region_name: str = "us-east-1") -> list:
|
||||||
|
"""
|
||||||
|
Scan a DynamoDB table and return all items.
|
||||||
|
|
||||||
|
Uses pagination to handle tables larger than 1MB response limit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the DynamoDB table to scan
|
||||||
|
region_name: AWS region where the table is located
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of all items in the table
|
||||||
|
"""
|
||||||
|
dynamodb = get_dynamodb_resource(region_name)
|
||||||
|
table = dynamodb.Table(table_name)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
last_evaluated_key = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if last_evaluated_key:
|
||||||
|
response = table.scan(ExclusiveStartKey=last_evaluated_key)
|
||||||
|
else:
|
||||||
|
response = table.scan()
|
||||||
|
|
||||||
|
items.extend(response.get("Items", []))
|
||||||
|
|
||||||
|
last_evaluated_key = response.get("LastEvaluatedKey")
|
||||||
|
if not last_evaluated_key:
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"Successfully scanned {len(items)} items from table '{table_name}'")
|
||||||
|
return items
|
||||||
|
|
||||||
|
except ClientError as e:
|
||||||
|
error_code = e.response["Error"]["Code"]
|
||||||
|
error_message = e.response["Error"]["Message"]
|
||||||
|
print(f"Error scanning table: {error_code} - {error_message}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def list_tables(region_name: str = "us-east-1") -> list:
|
||||||
|
"""List all DynamoDB tables in the specified region."""
|
||||||
|
client = get_dynamodb_client(region_name)
|
||||||
|
|
||||||
|
tables = []
|
||||||
|
last_evaluated_table_name = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if last_evaluated_table_name:
|
||||||
|
response = client.list_tables(ExclusiveStartTableName=last_evaluated_table_name)
|
||||||
|
else:
|
||||||
|
response = client.list_tables()
|
||||||
|
|
||||||
|
tables.extend(response.get("TableNames", []))
|
||||||
|
|
||||||
|
last_evaluated_table_name = response.get("LastEvaluatedTableName")
|
||||||
|
if not last_evaluated_table_name:
|
||||||
|
break
|
||||||
|
|
||||||
|
return tables
|
||||||
|
|
||||||
|
except ClientError as e:
|
||||||
|
error_code = e.response["Error"]["Code"]
|
||||||
|
error_message = e.response["Error"]["Message"]
|
||||||
|
print(f"Error listing tables: {error_code} - {error_message}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_table_info(table_name: str, region_name: str = "us-east-1") -> dict:
|
||||||
|
"""Get metadata information about a DynamoDB table."""
|
||||||
|
client = get_dynamodb_client(region_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = client.describe_table(TableName=table_name)
|
||||||
|
table_info = response.get("Table", {})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"TableName": table_info.get("TableName"),
|
||||||
|
"TableStatus": table_info.get("TableStatus"),
|
||||||
|
"ItemCount": table_info.get("ItemCount"),
|
||||||
|
"TableSizeBytes": table_info.get("TableSizeBytes"),
|
||||||
|
"KeySchema": table_info.get("KeySchema"),
|
||||||
|
"AttributeDefinitions": table_info.get("AttributeDefinitions"),
|
||||||
|
"CreationDateTime": str(table_info.get("CreationDateTime")),
|
||||||
|
}
|
||||||
|
|
||||||
|
except ClientError as e:
|
||||||
|
error_code = e.response["Error"]["Code"]
|
||||||
|
error_message = e.response["Error"]["Message"]
|
||||||
|
print(f"Error describing table: {error_code} - {error_message}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def read_table_as_xml(table_name: str, region_name: str = "us-east-1") -> str:
|
||||||
|
"""
|
||||||
|
Read all entries from a DynamoDB table and return as XML string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
table_name: Name of the DynamoDB table to read
|
||||||
|
region_name: AWS region where the table is located (default: us-east-1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
XML formatted string with all items wrapped in <period> tags
|
||||||
|
"""
|
||||||
|
items = scan_table(table_name, region_name)
|
||||||
|
return format_items_to_xml(items)
|
||||||
|
if __name__=="__main__":
|
||||||
|
print(read_table_as_xml("poc_dnx_monthly_summary","us-east-1"))
|
||||||
29
scripts/secretsmanager.py
Normal file
29
scripts/secretsmanager.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from botocore.exceptions import ClientError
|
||||||
|
import json
|
||||||
|
import boto3
|
||||||
|
from langfuse import Langfuse
|
||||||
|
def get_secret():
|
||||||
|
|
||||||
|
secret_name = "assistente-db-secrets-manager"
|
||||||
|
region_name = "us-east-1"
|
||||||
|
|
||||||
|
# Create a Secrets Manager client
|
||||||
|
session = boto3.session.Session()
|
||||||
|
client = session.client(
|
||||||
|
service_name='secretsmanager',
|
||||||
|
region_name=region_name
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_secret_value_response = client.get_secret_value(
|
||||||
|
SecretId=secret_name
|
||||||
|
)
|
||||||
|
except ClientError as e:
|
||||||
|
# For a list of exceptions thrown, see
|
||||||
|
# https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
|
||||||
|
raise e
|
||||||
|
|
||||||
|
secret = get_secret_value_response['SecretString']
|
||||||
|
return secret
|
||||||
|
secrets=json.loads(get_secret())['LANGFUSE-SECRET-KEY']
|
||||||
|
print(secrets)
|
||||||
322
scripts/simple_agent_with_langfuse.py
Normal file
322
scripts/simple_agent_with_langfuse.py
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple LangGraph Agent with Langfuse Integration
|
||||||
|
Demonstrates basic agent functionality with Langfuse observability
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import boto3
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
from typing import TypedDict, Annotated
|
||||||
|
import operator
|
||||||
|
|
||||||
|
from langgraph.graph import StateGraph, END
|
||||||
|
from langchain_aws import ChatBedrock
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage, SystemMessage
|
||||||
|
|
||||||
|
from langfuse import Langfuse
|
||||||
|
from langfuse.langchain import CallbackHandler
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# SECRETS MANAGER
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
def get_secret():
|
||||||
|
"""Fetch secrets from AWS Secrets Manager"""
|
||||||
|
secret_name = "assistente-db-secrets-manager"
|
||||||
|
region_name = "us-east-1"
|
||||||
|
|
||||||
|
session = boto3.session.Session()
|
||||||
|
client = session.client(
|
||||||
|
service_name='secretsmanager',
|
||||||
|
region_name=region_name
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
get_secret_value_response = client.get_secret_value(
|
||||||
|
SecretId=secret_name
|
||||||
|
)
|
||||||
|
except ClientError as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
secret = get_secret_value_response['SecretString']
|
||||||
|
return json.loads(secret)
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# INITIALIZE LANGFUSE
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
print("Initializing Langfuse...")
|
||||||
|
secrets = get_secret()
|
||||||
|
|
||||||
|
langfuse = Langfuse(
|
||||||
|
public_key=secrets['LANGFUSE-PUBLIC-KEY'],
|
||||||
|
secret_key=secrets['LANGFUSE-SECRET-KEY'],
|
||||||
|
host="http://98.92.98.83:3000"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✓ Langfuse initialized successfully")
|
||||||
|
print(f" Host: http://98.92.98.83:3000")
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# DEFINE TOOLS
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def add_numbers(a: int, b: int) -> int:
|
||||||
|
"""
|
||||||
|
Add two numbers together.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a: First number
|
||||||
|
b: Second number
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The sum of a and b
|
||||||
|
"""
|
||||||
|
print(f"🔧 [TOOL] Adding {a} + {b}")
|
||||||
|
return a + b
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def multiply_numbers(a: int, b: int) -> int:
|
||||||
|
"""
|
||||||
|
Multiply two numbers together.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
a: First number
|
||||||
|
b: Second number
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The product of a and b
|
||||||
|
"""
|
||||||
|
print(f"🔧 [TOOL] Multiplying {a} * {b}")
|
||||||
|
return a * b
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# AGENT STATE
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
class AgentState(TypedDict):
|
||||||
|
messages: Annotated[list, operator.add]
|
||||||
|
current_step: str
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# AGENT NODES
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
def create_bedrock_llm(inference_profile_arn: str, region: str = "us-east-1"):
|
||||||
|
"""Create a ChatBedrock instance with tools"""
|
||||||
|
llm = ChatBedrock(
|
||||||
|
model_id=inference_profile_arn,
|
||||||
|
region_name=region,
|
||||||
|
model_kwargs={
|
||||||
|
"max_tokens": 2048,
|
||||||
|
"temperature": 0.7,
|
||||||
|
},
|
||||||
|
provider="anthropic"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bind tools to the LLM
|
||||||
|
tools = [add_numbers, multiply_numbers]
|
||||||
|
llm_with_tools = llm.bind_tools(tools)
|
||||||
|
|
||||||
|
return llm_with_tools
|
||||||
|
|
||||||
|
|
||||||
|
def call_model(state: AgentState, llm) -> AgentState:
|
||||||
|
"""Call the LLM with Langfuse callback"""
|
||||||
|
print(f"[MODEL] Calling Bedrock with Langfuse tracing...")
|
||||||
|
|
||||||
|
messages = state["messages"]
|
||||||
|
|
||||||
|
# Create Langfuse callback handler
|
||||||
|
langfuse_handler = CallbackHandler()
|
||||||
|
config = {
|
||||||
|
"configurable": {"thread_id": "simple_agent_demo"},
|
||||||
|
"callbacks": [langfuse_handler]
|
||||||
|
}
|
||||||
|
|
||||||
|
response = llm.invoke(messages, config=config)
|
||||||
|
state["current_step"] = "model_called"
|
||||||
|
return {"messages": [response]}
|
||||||
|
|
||||||
|
|
||||||
|
def call_tools(state: AgentState) -> AgentState:
|
||||||
|
"""Execute any tool calls from the LLM response"""
|
||||||
|
print(f"[TOOLS] Checking for tool calls...")
|
||||||
|
|
||||||
|
messages = state["messages"]
|
||||||
|
last_message = messages[-1]
|
||||||
|
|
||||||
|
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
||||||
|
print(f"[TOOLS] Found {len(last_message.tool_calls)} tool call(s)")
|
||||||
|
|
||||||
|
tool_messages = []
|
||||||
|
tools_map = {
|
||||||
|
"add_numbers": add_numbers,
|
||||||
|
"multiply_numbers": multiply_numbers
|
||||||
|
}
|
||||||
|
|
||||||
|
for tool_call in last_message.tool_calls:
|
||||||
|
tool_name = tool_call["name"]
|
||||||
|
tool_args = tool_call["args"]
|
||||||
|
|
||||||
|
print(f"[TOOLS] Executing: {tool_name}({tool_args})")
|
||||||
|
|
||||||
|
tool_func = tools_map[tool_name]
|
||||||
|
result = tool_func.invoke(tool_args)
|
||||||
|
|
||||||
|
tool_message = ToolMessage(
|
||||||
|
content=str(result),
|
||||||
|
tool_call_id=tool_call["id"]
|
||||||
|
)
|
||||||
|
tool_messages.append(tool_message)
|
||||||
|
|
||||||
|
state["current_step"] = "tools_executed"
|
||||||
|
return {"messages": tool_messages}
|
||||||
|
else:
|
||||||
|
print(f"[TOOLS] No tool calls found")
|
||||||
|
state["current_step"] = "no_tools"
|
||||||
|
return {"messages": []}
|
||||||
|
|
||||||
|
|
||||||
|
def should_continue(state: AgentState) -> str:
|
||||||
|
"""Determine if we should continue to tools or end"""
|
||||||
|
messages = state["messages"]
|
||||||
|
last_message = messages[-1]
|
||||||
|
|
||||||
|
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
||||||
|
print("[ROUTER] Routing to tools...")
|
||||||
|
return "tools"
|
||||||
|
|
||||||
|
print("[ROUTER] No more tool calls, ending...")
|
||||||
|
return "end"
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# CREATE AGENT
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
def create_agent(inference_profile_arn: str, region: str = "us-east-1"):
|
||||||
|
"""Create a LangGraph agent with Langfuse observability"""
|
||||||
|
|
||||||
|
llm = create_bedrock_llm(inference_profile_arn, region)
|
||||||
|
|
||||||
|
workflow = StateGraph(AgentState)
|
||||||
|
|
||||||
|
# Add nodes
|
||||||
|
workflow.add_node("model", lambda state: call_model(state, llm))
|
||||||
|
workflow.add_node("tools", call_tools)
|
||||||
|
|
||||||
|
# Define workflow
|
||||||
|
workflow.set_entry_point("model")
|
||||||
|
|
||||||
|
workflow.add_conditional_edges(
|
||||||
|
"model",
|
||||||
|
should_continue,
|
||||||
|
{
|
||||||
|
"tools": "tools",
|
||||||
|
"end": END
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.add_edge("tools", "model")
|
||||||
|
|
||||||
|
app = workflow.compile()
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# MAIN EXECUTION
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main execution function"""
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("Simple LangGraph Agent with Langfuse")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
INFERENCE_PROFILE_ARN = "arn:aws:bedrock:us-east-1:305427701314:application-inference-profile/b3umwd5jpd0u"
|
||||||
|
REGION = "us-east-1"
|
||||||
|
|
||||||
|
# System prompt
|
||||||
|
SYSTEM_PROMPT = """You are a helpful math assistant with access to calculation tools.
|
||||||
|
|
||||||
|
Your available tools:
|
||||||
|
- add_numbers: Add two numbers
|
||||||
|
- multiply_numbers: Multiply two numbers
|
||||||
|
|
||||||
|
When a user asks you to perform calculations:
|
||||||
|
1. Break down the problem into steps
|
||||||
|
2. Use the appropriate tools
|
||||||
|
3. Show your reasoning clearly
|
||||||
|
4. Provide the final answer
|
||||||
|
|
||||||
|
Always use the tools rather than calculating in your head."""
|
||||||
|
|
||||||
|
print(f"\nUsing inference profile: {INFERENCE_PROFILE_ARN}")
|
||||||
|
print(f"Region: {REGION}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Create the agent
|
||||||
|
agent = create_agent(INFERENCE_PROFILE_ARN, REGION)
|
||||||
|
|
||||||
|
# Example query
|
||||||
|
user_query = "What is (10 + 5) * 3?"
|
||||||
|
|
||||||
|
# Initialize state
|
||||||
|
initial_state = {
|
||||||
|
"messages": [
|
||||||
|
SystemMessage(content=SYSTEM_PROMPT),
|
||||||
|
HumanMessage(content=user_query)
|
||||||
|
],
|
||||||
|
"current_step": "init"
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"\nUser Query: {user_query}\n")
|
||||||
|
print("-" * 60)
|
||||||
|
|
||||||
|
# Run the agent
|
||||||
|
final_state = agent.invoke(initial_state)
|
||||||
|
|
||||||
|
# Display results
|
||||||
|
print("-" * 60)
|
||||||
|
print("\n[FINAL RESULT]\n")
|
||||||
|
|
||||||
|
for i, msg in enumerate(final_state["messages"], 1):
|
||||||
|
if isinstance(msg, SystemMessage):
|
||||||
|
print(f"{i}. System: [System prompt configured]")
|
||||||
|
elif isinstance(msg, HumanMessage):
|
||||||
|
print(f"{i}. User: {msg.content}")
|
||||||
|
elif isinstance(msg, AIMessage):
|
||||||
|
if hasattr(msg, 'tool_calls') and msg.tool_calls:
|
||||||
|
print(f"{i}. AI: [Calling tools...]")
|
||||||
|
else:
|
||||||
|
print(f"{i}. AI: {msg.content}")
|
||||||
|
elif isinstance(msg, ToolMessage):
|
||||||
|
print(f"{i}. Tool Result: {msg.content}")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print(f"Agent completed successfully!")
|
||||||
|
|
||||||
|
# Flush Langfuse data
|
||||||
|
print("\nFlushing data to Langfuse...")
|
||||||
|
langfuse.flush()
|
||||||
|
print("✓ Data sent to Langfuse")
|
||||||
|
|
||||||
|
print("\nView your traces at: http://98.92.98.83:3000")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
12
scripts/test_api.sh
Executable file
12
scripts/test_api.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
URL="http://alb-assistente-analitico-7e352f9-1039635730.us-east-1.elb.amazonaws.com:8000"
|
||||||
|
|
||||||
|
curl -X POST "${URL}/agent" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"query": "Qual o NPS de dezembro 2025?",
|
||||||
|
"history": "",
|
||||||
|
"model": "anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||||
|
"base": "bacio_transacional_loja_app"
|
||||||
|
}'
|
||||||
133
scripts/test_langfuse_connection.py
Normal file
133
scripts/test_langfuse_connection.py
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to diagnose Langfuse connectivity issues
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
print("=" * 60)
|
||||||
|
print("Langfuse Connection Diagnostic Tool")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
# Test 1: Check if required modules are installed
|
||||||
|
print("\n1. Checking required modules...")
|
||||||
|
try:
|
||||||
|
import boto3
|
||||||
|
print(" ✓ boto3 installed")
|
||||||
|
except ImportError:
|
||||||
|
print(" ✗ boto3 NOT installed")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from langfuse import Langfuse
|
||||||
|
print(" ✓ langfuse installed")
|
||||||
|
except ImportError:
|
||||||
|
print(" ✗ langfuse NOT installed")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from botocore.exceptions import ClientError
|
||||||
|
print(" ✓ botocore installed")
|
||||||
|
except ImportError:
|
||||||
|
print(" ✗ botocore NOT installed")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Test 2: Fetch secrets from AWS Secrets Manager
|
||||||
|
print("\n2. Fetching secrets from AWS Secrets Manager...")
|
||||||
|
secrets = None
|
||||||
|
try:
|
||||||
|
secret_name = "assistente-db-secrets-manager"
|
||||||
|
region_name = "us-east-1"
|
||||||
|
|
||||||
|
session = boto3.session.Session()
|
||||||
|
client = session.client(
|
||||||
|
service_name='secretsmanager',
|
||||||
|
region_name=region_name
|
||||||
|
)
|
||||||
|
|
||||||
|
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
|
||||||
|
secret = get_secret_value_response['SecretString']
|
||||||
|
secrets = json.loads(secret)
|
||||||
|
|
||||||
|
print(f" ✓ Successfully fetched secrets")
|
||||||
|
print(f" ✓ Found keys: {list(secrets.keys())}")
|
||||||
|
|
||||||
|
# Check for Langfuse keys
|
||||||
|
has_public_key = 'LANGFUSE-PUBLIC-KEY' in secrets
|
||||||
|
has_secret_key = 'LANGFUSE-SECRET-KEY' in secrets
|
||||||
|
|
||||||
|
print(f" {'✓' if has_public_key else '✗'} LANGFUSE-PUBLIC-KEY {'found' if has_public_key else 'MISSING'}")
|
||||||
|
print(f" {'✓' if has_secret_key else '✗'} LANGFUSE-SECRET-KEY {'found' if has_secret_key else 'MISSING'}")
|
||||||
|
|
||||||
|
if not has_public_key or not has_secret_key:
|
||||||
|
print("\n ⚠ Langfuse credentials not found in Secrets Manager!")
|
||||||
|
secrets = None
|
||||||
|
|
||||||
|
except ClientError as e:
|
||||||
|
print(f" ⚠ Error fetching secrets: {e}")
|
||||||
|
print(" Will try with manual input...")
|
||||||
|
secrets = None
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ⚠ Unexpected error: {e}")
|
||||||
|
print(" Will try with manual input...")
|
||||||
|
secrets = None
|
||||||
|
|
||||||
|
# If AWS Secrets Manager failed, try manual input
|
||||||
|
if secrets is None:
|
||||||
|
print("\n Please provide Langfuse credentials manually:")
|
||||||
|
public_key = input(" Enter LANGFUSE-PUBLIC-KEY: ").strip()
|
||||||
|
secret_key = input(" Enter LANGFUSE-SECRET-KEY: ").strip()
|
||||||
|
|
||||||
|
if not public_key or not secret_key:
|
||||||
|
print(" ✗ Both keys are required!")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
secrets = {
|
||||||
|
'LANGFUSE-PUBLIC-KEY': public_key,
|
||||||
|
'LANGFUSE-SECRET-KEY': secret_key
|
||||||
|
}
|
||||||
|
print(" ✓ Manual credentials provided")
|
||||||
|
|
||||||
|
# Test 3: Initialize Langfuse client
|
||||||
|
print("\n3. Initializing Langfuse client...")
|
||||||
|
try:
|
||||||
|
host = "http://98.92.98.83:3000"
|
||||||
|
langfuse = Langfuse(
|
||||||
|
public_key=secrets['LANGFUSE-PUBLIC-KEY'],
|
||||||
|
secret_key=secrets['LANGFUSE-SECRET-KEY'],
|
||||||
|
host=host
|
||||||
|
)
|
||||||
|
print(f" ✓ Langfuse client initialized with host: {host}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Error initializing Langfuse: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Test 4: Test API connectivity
|
||||||
|
print("\n4. Testing Langfuse API connectivity...")
|
||||||
|
try:
|
||||||
|
# Create a simple test trace
|
||||||
|
trace = langfuse.trace(
|
||||||
|
name="connection_test",
|
||||||
|
user_id="diagnostic_script"
|
||||||
|
)
|
||||||
|
print(" ✓ Test trace created")
|
||||||
|
|
||||||
|
# Try to flush
|
||||||
|
langfuse.flush()
|
||||||
|
print(" ✓ Data flushed to Langfuse")
|
||||||
|
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("SUCCESS: Langfuse connection is working properly!")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ✗ Error communicating with Langfuse: {e}")
|
||||||
|
print(f"\n Error details: {type(e).__name__}: {str(e)}")
|
||||||
|
|
||||||
|
# Additional diagnostic info
|
||||||
|
print("\n Troubleshooting tips:")
|
||||||
|
print(" - Verify the Langfuse server is running")
|
||||||
|
print(" - Check if the host URL is correct")
|
||||||
|
print(" - Verify your API keys are valid in Langfuse UI")
|
||||||
|
print(" - Check network connectivity to the Langfuse server")
|
||||||
|
sys.exit(1)
|
||||||
53
scripts/teste.py
Normal file
53
scripts/teste.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import boto3
|
||||||
|
import time
|
||||||
|
|
||||||
|
WORKGROUP = "iceberg-workgroup"
|
||||||
|
DATABASE = "dnx_warehouse"
|
||||||
|
|
||||||
|
session = boto3.Session()
|
||||||
|
athena = session.client("athena", region_name="us-east-1")
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# QUERY
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
QUERY = """
|
||||||
|
SELECT title,shortname from AwsDataCatalog.dnx_warehouse.bacio_transacional_loja_app_pesquisa;
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("Executando query no Athena...")
|
||||||
|
response = athena.start_query_execution(
|
||||||
|
QueryString=QUERY,
|
||||||
|
QueryExecutionContext={"Database": DATABASE},
|
||||||
|
WorkGroup=WORKGROUP
|
||||||
|
)
|
||||||
|
|
||||||
|
query_execution_id = response["QueryExecutionId"]
|
||||||
|
print(f"QueryExecutionId: {query_execution_id}")
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# AGUARDAR RESULTADO
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
while True:
|
||||||
|
result = athena.get_query_execution(QueryExecutionId=query_execution_id)
|
||||||
|
state = result["QueryExecution"]["Status"]["State"]
|
||||||
|
|
||||||
|
if state in ["SUCCEEDED", "FAILED", "CANCELLED"]:
|
||||||
|
print("Estado final:", state)
|
||||||
|
break
|
||||||
|
|
||||||
|
print("Aguardando execução...")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# RESULTADO
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
if state == "SUCCEEDED":
|
||||||
|
output = athena.get_query_results(QueryExecutionId=query_execution_id)
|
||||||
|
print("\nResultados:")
|
||||||
|
for row in output["ResultSet"]["Rows"]:
|
||||||
|
print([col.get("VarCharValue", "") for col in row["Data"]][0])
|
||||||
|
else:
|
||||||
|
print("Erro ao executar a query.")
|
||||||
Reference in New Issue
Block a user