Initial commit

This commit is contained in:
2026-05-14 15:29:03 -03:00
parent 82ac556ecc
commit 54bcf081f6
31 changed files with 3132 additions and 518 deletions

View File

@@ -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
# Copia os arquivos de requisitos primeiro para aproveitar o cache do Docker
COPY requirements.txt ./requirements.txt
# Instala as dependências do backend
RUN pip install --no-cache-dir -r requirements.txt
# Copia o restante dos diretórios e arquivos da aplicação
COPY ./ ./
# Garante que o script de inicialização seja executável
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
EXPOSE 8000
# Porta 8501 para o aplicativo Streamlit
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"]

View File

@@ -1,6 +1,6 @@
from fastapi import FastAPI
from pydantic import BaseModel
from .backend import BDAgent
from .backend import orquestrador
app = FastAPI()
@@ -26,7 +26,7 @@ class QueryResponse(BaseModel):
@app.post("/agent", response_model=QueryResponse)
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(
response=result["response"],
input_tokens=result["input_tokens"],

View File

@@ -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")

View File

View 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()

View 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"]

View 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": {}}

View 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
View 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.",
),
]

View File

@@ -1,6 +1,6 @@
import streamlit as st
import time
from backend import BDAgent
from backend import orquestrador
import boto3
from boto3.dynamodb.conditions import Key
@@ -12,7 +12,7 @@ st.set_page_config(
)
session = boto3.Session()
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
dynamodb = boto3.resource("dynamodb", region_name=orquestrador.REGION)
def list_bases():
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)
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"]
# Simulate typing effect

View File

@@ -1,8 +1,8 @@
boto3>=1.34.0
langchain-aws>=0.1.0
langgraph>=0.0.20
langchain>=0.1.0
streamlit
langfuse
fastapi
uvicorn
boto3==1.42.10
langchain-aws==1.1.0
langgraph==1.0.5
langchain==1.2.0
streamlit==1.52.2
langfuse==3.11.2
fastapi==0.129.0
uvicorn==0.41.0

52
docs/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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: 13 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 | 13 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
View 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 15, 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
View 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
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

View File

@@ -26,7 +26,7 @@ config:
- task_name: assisnte-analitico-db-dev
ecr_repo_name: assistente-analitico-db-dev
ecr_image_tag: latest
ecr_image_digest: sha256:c9cd83a7caf51e7eee471d47649bbe0157d4c2703f365010f12997e63d941d3d
ecr_image_digest: sha256:0bd3a927df4367ba29dbd173e0414d884e973c37599a3f6241341e8d190e827b
cpu: 256
memory: 512
desired_count: 1
@@ -47,6 +47,10 @@ config:
container_port: 8000
env_variables:
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
# BEDROCK_REGION: us-east-1
# LANGCHAIN_TRACING_V2: "true"

View 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...
"""

View 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())

View 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
View 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)

View 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
View 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"
}'

View 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
View 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.")