Initial commit
This commit is contained in:
@@ -1,37 +1,16 @@
|
||||
# Use uma imagem base Python oficial.
|
||||
# Escolha uma versão que seja compatível com suas dependências.
|
||||
FROM python:3.12-slim
|
||||
|
||||
# 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"]
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -1,482 +0,0 @@
|
||||
"""
|
||||
LangGraph Agent using AWS Bedrock Cross-Region Inference Profile with Tools
|
||||
|
||||
This script demonstrates how to create a LangGraph agent that uses
|
||||
an AWS Bedrock inference profile with custom tools (add and multiply).
|
||||
"""
|
||||
import boto3
|
||||
from typing import TypedDict, Annotated
|
||||
from langgraph.graph import StateGraph, END
|
||||
from langchain_aws import ChatBedrockConverse
|
||||
from langchain_core.tools import tool
|
||||
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage, SystemMessage
|
||||
import operator
|
||||
import json
|
||||
import time
|
||||
from langfuse import Langfuse
|
||||
from langfuse.langchain import CallbackHandler
|
||||
from botocore.exceptions import ClientError
|
||||
import os
|
||||
WORKGROUP = "iceberg-workgroup"
|
||||
DATABASE = "dnx_warehouse"
|
||||
TABLE = "poc_dnx_monthly_summary"
|
||||
REGION = "us-east-1"
|
||||
|
||||
# DynamoDB client
|
||||
dynamodb = boto3.resource("dynamodb", region_name=REGION)
|
||||
|
||||
|
||||
@tool
|
||||
def get_monthly_report(id: str, variable: str) -> str:
|
||||
"""
|
||||
Get a specific variable's data from DynamoDB for a specific id.
|
||||
|
||||
Args:
|
||||
id: The id of the data
|
||||
variable: The variable/column name to retrieve from the table
|
||||
|
||||
Returns:
|
||||
The content of the specified variable for the given id
|
||||
"""
|
||||
print(f"\n🔧 [TOOL CALLED] get_monthly_report for month: {id}, variable: {variable}")
|
||||
|
||||
try:
|
||||
table = dynamodb.Table(TABLE)
|
||||
response = table.get_item(Key={"id": id})
|
||||
|
||||
if "Item" not in response:
|
||||
return f"No report found for month: {id}"
|
||||
|
||||
item = response["Item"]
|
||||
content = item.get(variable, "")
|
||||
|
||||
if not content:
|
||||
return f"Variable '{variable}' not found for month: {id}"
|
||||
|
||||
result = f"<{id}>\n{content}\n</{id}>"
|
||||
return result
|
||||
|
||||
except ClientError as e:
|
||||
error_message = e.response["Error"]["Message"]
|
||||
return f"Error fetching report: {error_message}"
|
||||
|
||||
|
||||
@tool
|
||||
def get_consolidated_keys(id: str) -> str:
|
||||
"""
|
||||
Get the list of consolidated keys (variables) available in the table for a specific month.
|
||||
|
||||
Args:
|
||||
id: The id of the data
|
||||
|
||||
Returns:
|
||||
The list of available variables/keys for the specified data
|
||||
"""
|
||||
print(f"\n🔧 [TOOL CALLED] get_consolidated_keys for id: {id}")
|
||||
|
||||
try:
|
||||
table = dynamodb.Table(TABLE)
|
||||
response = table.get_item(Key={"id": id})
|
||||
|
||||
if "Item" not in response:
|
||||
return f"No data found for month: {id}"
|
||||
|
||||
item = response["Item"]
|
||||
chaves_consolidadas = item.get("chaves_consolidadas", "")
|
||||
|
||||
if not chaves_consolidadas:
|
||||
return f"No consolidated keys found for id: {id}"
|
||||
|
||||
return chaves_consolidadas
|
||||
|
||||
except ClientError as e:
|
||||
error_message = e.response["Error"]["Message"]
|
||||
return f"Error fetching consolidated keys: {error_message}"
|
||||
|
||||
|
||||
def get_contexto() -> dict:
|
||||
"""
|
||||
Get contexto, filter, and items_disponiveis from DynamoDB where id=DASHBOARD+'_contexto'.
|
||||
|
||||
Returns:
|
||||
Dict with 'contexto', 'filter', and 'items_disponiveis' keys
|
||||
"""
|
||||
try:
|
||||
table = dynamodb.Table(TABLE)
|
||||
response = table.get_item(Key={"id": DASHBOARD + "_contexto"})
|
||||
|
||||
if "Item" not in response:
|
||||
return {"contexto": "", "filter": "", "items_disponiveis": {}}
|
||||
|
||||
item = response["Item"]
|
||||
return {
|
||||
"contexto": item.get("contexto", ""),
|
||||
"filter": item.get("filter_key", ""),
|
||||
"items_disponiveis": item.get("itens_disponiveis", {}),
|
||||
}
|
||||
|
||||
except ClientError as e:
|
||||
error_message = e.response["Error"]["Message"]
|
||||
return {"contexto": f"Error: {error_message}", "filter": "", "items_disponiveis": {}}
|
||||
|
||||
|
||||
def get_secret():
|
||||
|
||||
secret_name = "assistente-db-secrets-manager"
|
||||
region_name = "us-east-1"
|
||||
|
||||
# Create a Secrets Manager client
|
||||
session = boto3.session.Session()
|
||||
client = session.client(
|
||||
service_name='secretsmanager',
|
||||
region_name=region_name
|
||||
)
|
||||
|
||||
try:
|
||||
get_secret_value_response = client.get_secret_value(
|
||||
SecretId=secret_name
|
||||
)
|
||||
except ClientError as e:
|
||||
# For a list of exceptions thrown, see
|
||||
# https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
|
||||
raise e
|
||||
|
||||
secret = get_secret_value_response['SecretString']
|
||||
return secret
|
||||
secrets=json.loads(get_secret())
|
||||
langfuse = Langfuse(
|
||||
public_key=secrets['LANGFUSE-PUBLIC-KEY'],
|
||||
secret_key=secrets['LANGFUSE-SECRET-KEY'],
|
||||
host=os.environ["LANGFUSE_HOST"]
|
||||
)
|
||||
session = boto3.Session()
|
||||
athena = session.client("athena", region_name="us-east-1")
|
||||
|
||||
# ==============================================
|
||||
# QUERY
|
||||
# ==============================================
|
||||
|
||||
def exec_athena_query(query):
|
||||
print("Executando query no Athena...")
|
||||
response = athena.start_query_execution(
|
||||
QueryString=query,
|
||||
QueryExecutionContext={"Database": DATABASE},
|
||||
WorkGroup=WORKGROUP
|
||||
)
|
||||
|
||||
query_execution_id = response["QueryExecutionId"]
|
||||
print(f"QueryExecutionId: {query_execution_id}")
|
||||
|
||||
# ==============================================
|
||||
# AGUARDAR RESULTADO
|
||||
# ==============================================
|
||||
|
||||
while True:
|
||||
result = athena.get_query_execution(QueryExecutionId=query_execution_id)
|
||||
state = result["QueryExecution"]["Status"]["State"]
|
||||
|
||||
if state in ["SUCCEEDED", "FAILED", "CANCELLED"]:
|
||||
print("Estado final:", state)
|
||||
break
|
||||
|
||||
print("Aguardando execução...")
|
||||
time.sleep(1)
|
||||
if state == "SUCCEEDED":
|
||||
output = athena.get_query_results(QueryExecutionId=query_execution_id)
|
||||
print(f"\n🔧 [TOOL CALLED] consult answer")
|
||||
return output["ResultSet"]["Rows"]
|
||||
else:
|
||||
print("Erro ao executar a query.")
|
||||
# Define tools
|
||||
|
||||
|
||||
|
||||
# Define@tool the agent state
|
||||
class AgentState(TypedDict):
|
||||
messages: Annotated[list, operator.add]
|
||||
current_step: str
|
||||
|
||||
|
||||
# Initialize Bedrock client with inference profile
|
||||
def create_bedrock_llm(model_id: str, region: str = "us-east-1"):
|
||||
"""
|
||||
Create a ChatBedrock instance using a model ID.
|
||||
|
||||
Args:
|
||||
model_id: Bedrock model ID (e.g., anthropic.claude-haiku-4-5-20251001-v1:0)
|
||||
region: AWS region (default: us-east-1)
|
||||
|
||||
Returns:
|
||||
ChatBedrock instance configured with the model
|
||||
"""
|
||||
# Determine provider and model_kwargs based on model ID
|
||||
MODEL_ARNS = {
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"meta.llama4-maverick-17b-instruct-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.meta.llama4-maverick-17b-instruct-v1:0",
|
||||
"meta.llama4-scout-17b-instruct-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.meta.llama4-scout-17b-instruct-v1:0",
|
||||
"amazon.nova-lite-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.amazon.nova-lite-v1:0",
|
||||
"amazon.nova-pro-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/us.amazon.nova-pro-v1:0",
|
||||
"amazon.nova-2-lite-v1:0": "arn:aws:bedrock:us-east-1:305427701314:inference-profile/global.amazon.nova-2-lite-v1:0"
|
||||
}
|
||||
PROVIDER={
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": "anthropic",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": "anthropic",
|
||||
"meta.llama4-maverick-17b-instruct-v1:0": "meta",
|
||||
"meta.llama4-scout-17b-instruct-v1:0": "meta",
|
||||
"amazon.nova-lite-v1:0": "amazon",
|
||||
"amazon.nova-pro-v1:0": "amazon",
|
||||
"amazon.nova-2-lite-v1:0": "amazon"
|
||||
}
|
||||
prefix={
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": "us",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": "global",
|
||||
"meta.llama4-maverick-17b-instruct-v1:0": "us",
|
||||
"meta.llama4-scout-17b-instruct-v1:0": "us",
|
||||
"amazon.nova-lite-v1:0": "us",
|
||||
"amazon.nova-pro-v1:0": "us",
|
||||
"amazon.nova-2-lite-v1:0": "global"
|
||||
}
|
||||
llm = ChatBedrockConverse(
|
||||
model_id=prefix[model_id]+"."+model_id,
|
||||
region_name=region,
|
||||
provider=PROVIDER[model_id],
|
||||
max_tokens=2048,
|
||||
temperature=0.7
|
||||
)
|
||||
|
||||
# Bind tools to the LLM
|
||||
tools = [get_monthly_report, get_consolidated_keys]
|
||||
llm_with_tools = llm.bind_tools(tools)
|
||||
|
||||
return llm_with_tools
|
||||
|
||||
|
||||
# Define agent nodes
|
||||
def call_model(state: AgentState, llm) -> AgentState:
|
||||
"""Call the LLM with tools."""
|
||||
print(f"[MODEL] Calling Bedrock inference profile...")
|
||||
|
||||
messages = state["messages"]
|
||||
response = llm.invoke(messages)
|
||||
state["current_step"] = "model_called"
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
def call_tools(state: AgentState) -> AgentState:
|
||||
"""Execute any tool calls from the LLM response."""
|
||||
print(f"[TOOLS] Checking for tool calls...")
|
||||
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1]
|
||||
|
||||
# Check if there are tool calls
|
||||
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
||||
print(f"[TOOLS] Found {len(last_message.tool_calls)} tool call(s)")
|
||||
|
||||
tool_messages = []
|
||||
tools_map = {
|
||||
"get_monthly_report": get_monthly_report,
|
||||
"get_consolidated_keys": get_consolidated_keys
|
||||
}
|
||||
|
||||
# Execute each tool call
|
||||
for tool_call in last_message.tool_calls:
|
||||
tool_name = tool_call["name"]
|
||||
tool_args = tool_call["args"]
|
||||
|
||||
print(f"[TOOLS] Executing: {tool_name}")
|
||||
|
||||
# Call the appropriate tool
|
||||
tool_func = tools_map[tool_name]
|
||||
result = tool_func.invoke(tool_args)
|
||||
|
||||
# Create tool message
|
||||
tool_message = ToolMessage(
|
||||
content=str(result),
|
||||
tool_call_id=tool_call["id"]
|
||||
)
|
||||
tool_messages.append(tool_message)
|
||||
|
||||
state["current_step"] = "tools_executed"
|
||||
return {"messages": tool_messages}
|
||||
else:
|
||||
print(f"[TOOLS] No tool calls found")
|
||||
state["current_step"] = "no_tools"
|
||||
return {"messages": []}
|
||||
|
||||
|
||||
def should_continue(state: AgentState) -> str:
|
||||
"""Determine if we should continue to tools or end."""
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1]
|
||||
|
||||
# If there are tool calls, continue to tools node
|
||||
if hasattr(last_message, 'tool_calls') and last_message.tool_calls:
|
||||
print("[ROUTER] Routing to tools...")
|
||||
return "tools"
|
||||
|
||||
# Otherwise, end
|
||||
print("[ROUTER] No more tool calls, ending...")
|
||||
return "end"
|
||||
|
||||
|
||||
# Build the LangGraph agent
|
||||
def create_agent(inference_profile_arn: str, region: str = "us-east-1"):
|
||||
"""
|
||||
Create a LangGraph agent that uses Bedrock inference profile with tools.
|
||||
|
||||
Args:
|
||||
inference_profile_arn: ARN of the cross-region inference profile
|
||||
region: AWS region
|
||||
|
||||
Returns:
|
||||
Compiled LangGraph workflow
|
||||
"""
|
||||
# Initialize the LLM with tools
|
||||
llm = create_bedrock_llm(inference_profile_arn, region)
|
||||
|
||||
# Create the graph
|
||||
workflow = StateGraph(AgentState)
|
||||
|
||||
# Add nodes
|
||||
workflow.add_node("model", lambda state: call_model(state, llm))
|
||||
workflow.add_node("tools", call_tools)
|
||||
|
||||
# Define the workflow
|
||||
workflow.set_entry_point("model")
|
||||
|
||||
# Add conditional edges
|
||||
workflow.add_conditional_edges(
|
||||
"model",
|
||||
should_continue,
|
||||
{
|
||||
"tools": "tools",
|
||||
"end": END
|
||||
}
|
||||
)
|
||||
|
||||
# After tools, go back to model
|
||||
workflow.add_edge("tools", "model")
|
||||
|
||||
# Compile the graph
|
||||
app = workflow.compile()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def main(user_query,history,model,base):
|
||||
"""Main execution function."""
|
||||
global DASHBOARD
|
||||
DASHBOARD = base
|
||||
|
||||
# Configuration - Update with your actual inference profile ARN
|
||||
|
||||
INFERENCE_PROFILE_ARN = model
|
||||
REGION = "us-east-1"
|
||||
# System prompt for the agent
|
||||
|
||||
contexto_data = get_contexto()
|
||||
if contexto_data["filter"]=="period":
|
||||
CONSULT_RULES="""To use the tools you must give the id of the correspondant data, which can be associated to a given month and year in the following format year_month, which:
|
||||
-Year is the year in 4 digits (2025,2024,2023,2022,2021,...)
|
||||
-Month is th two digit representation: 01,02,03,04,05,06,07,08,09,10,11,12
|
||||
The format of the dict is: {id1:year_month1,id2:year_month2...}
|
||||
Choose the correct id based on the following dict:
|
||||
"""
|
||||
elif contexto_data["filter"]=="event":
|
||||
CONSULT_RULES="""To use the tools you must give the id of the correspondant data, which can be associated to a event, which is in the format "Name - City DD/MM/YYYY", where the last is a date in the format day/month/year. Theformat of elements in dict is {id1:event_description1,id2:event_description2...}"""
|
||||
else:
|
||||
CONSULT_RULES="""Wrong filter value, you must terminate the workflow and ask the user to contact the technical team"""
|
||||
SYSTEM_PROMPT=""" You are a analitical agent in Brazilian Portuguese, with acess to monthly reports about a specific company, specified in the context. You have access to tools that lets you consult present variables in table, you always have access to "context", which keeps inside answers to different questions, that you may consult as you desire.
|
||||
Do not access other variables besides the ones reported by the tool and "context".
|
||||
You currently have access to data in a period specified in the context, so only answer questions inside the time window.
|
||||
<context>
|
||||
"""+contexto_data["contexto"]+"""
|
||||
</context>
|
||||
"""+CONSULT_RULES+"""
|
||||
<correlation>
|
||||
"""+str(contexto_data["items_disponiveis"])+"""
|
||||
</correlation>
|
||||
Here is the chat history:"""+history+"""
|
||||
Inside the "NPS" in data is some useful values to calculate the NPS, which includes "distribuicao".
|
||||
Inside of it are grades and the amount of people who given that grade.
|
||||
Grades from 0 to 6 are detractors.
|
||||
Grades from 7 to 8 are neutral.
|
||||
Grades from 9 to 10 are promoters.
|
||||
Calculate the percentage of them when prompted about NPS and then calculate the nps using the following formula: NPS = %promoter - %detractor, never use the medium of the notes.
|
||||
|
||||
You have access to the tools:
|
||||
-get_consolidated_keys: Given a id returns the column names inside of a entity of a given table element.
|
||||
- get_monthly_report: given a id and a variable name, either one listed in the previous tool output or "context", returns its value. Using "context" gives you a summarization of many answers of questions asked to the customers.
|
||||
Answer, in Brazilian Portuguese, to the user the best you can with the given information, if you don't know the answer or how to answer say so, only answer from what you know.
|
||||
Always consult the most recent information when a date is not given, like questions "Quanto é meu nps?" """
|
||||
|
||||
print("=" * 60)
|
||||
print("LangGraph Agent with AWS Bedrock Inference Profile + Tools")
|
||||
print("=" * 60)
|
||||
print(f"\nUsing inference profile: {INFERENCE_PROFILE_ARN}")
|
||||
print(f"Region: {REGION}\n")
|
||||
print("Available Tools:")
|
||||
print(" - add_numbers(a, b): Add two numbers")
|
||||
print(" - multiply_numbers(a, b): Multiply two numbers")
|
||||
print("\nSystem Prompt: Configured ✓")
|
||||
print("=" * 60)
|
||||
|
||||
# Create the agent with a unique session_id to group all steps
|
||||
langfuse_handler = CallbackHandler()
|
||||
agent = create_agent(INFERENCE_PROFILE_ARN, REGION)
|
||||
|
||||
# Initialize state with system prompt
|
||||
initial_state = {
|
||||
"messages": [
|
||||
SystemMessage(content=SYSTEM_PROMPT),
|
||||
HumanMessage(content=user_query)
|
||||
],
|
||||
"current_step": "init"
|
||||
}
|
||||
|
||||
print(f"\nUser Query: {user_query}\n")
|
||||
print("-" * 60)
|
||||
|
||||
# Run the agent with callbacks at graph level
|
||||
config = {"callbacks": [langfuse_handler], "tags": [DASHBOARD]}
|
||||
final_state = agent.invoke(initial_state, config=config)
|
||||
|
||||
# Display results
|
||||
print("-" * 60)
|
||||
print("\n[FINAL RESULT]")
|
||||
print("\nConversation History:")
|
||||
for i, msg in enumerate(final_state["messages"], 1):
|
||||
if isinstance(msg, SystemMessage):
|
||||
print(f"\n{i}. System: [System prompt configured]")
|
||||
elif isinstance(msg, HumanMessage):
|
||||
print(f"\n{i}. User: {msg.content}")
|
||||
elif isinstance(msg, AIMessage):
|
||||
if hasattr(msg, 'tool_calls') and msg.tool_calls:
|
||||
print(f"\n{i}. AI: [Calling tools...]")
|
||||
else:
|
||||
print(f"\n{i}. AI: {msg.content}")
|
||||
elif isinstance(msg, ToolMessage):
|
||||
print(f"\n{i}. Tool Result: {msg.content}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Agent completed successfully. Final step: {final_state['current_step']}")
|
||||
|
||||
# Aggregate token usage from all AIMessage objects
|
||||
total_input_tokens = 0
|
||||
total_output_tokens = 0
|
||||
for msg in final_state["messages"]:
|
||||
if isinstance(msg, AIMessage) and hasattr(msg, 'usage_metadata') and msg.usage_metadata:
|
||||
total_input_tokens += msg.usage_metadata.get("input_tokens", 0)
|
||||
total_output_tokens += msg.usage_metadata.get("output_tokens", 0)
|
||||
|
||||
langfuse.flush()
|
||||
return {
|
||||
"response": final_state['messages'][-1].content,
|
||||
"input_tokens": total_input_tokens,
|
||||
"output_tokens": total_output_tokens,
|
||||
"total_tokens": total_input_tokens + total_output_tokens,
|
||||
}
|
||||
if __name__=="__main__":
|
||||
main("Liste o nps mês a mês desde maio 2025 até dezembro 2025","","anthropic.claude-sonnet-4-5-20250929-v1:0")
|
||||
0
code/app/backend/__init__.py
Normal file
0
code/app/backend/__init__.py
Normal file
120
code/app/backend/agent_bedrock.py
Normal file
120
code/app/backend/agent_bedrock.py
Normal file
@@ -0,0 +1,120 @@
|
||||
import operator
|
||||
from typing import TypedDict, Annotated
|
||||
|
||||
from langchain_aws import ChatBedrockConverse
|
||||
from langchain_core.messages import AIMessage, ToolMessage
|
||||
from langgraph.graph import StateGraph, END
|
||||
|
||||
from .config import REGION, AWS_ACCOUNT
|
||||
|
||||
|
||||
class AgentState(TypedDict):
|
||||
messages: Annotated[list, operator.add]
|
||||
current_step: str
|
||||
|
||||
|
||||
def create_bedrock_llm(model_id: str, region: str = REGION, tools: list = None):
|
||||
"""
|
||||
Create a ChatBedrock instance using a model ID.
|
||||
|
||||
Args:
|
||||
model_id: Bedrock model ID (e.g., anthropic.claude-haiku-4-5-20251001-v1:0)
|
||||
region: AWS region (default: REGION env var)
|
||||
tools: List of LangChain tools to bind to the model
|
||||
|
||||
Returns:
|
||||
ChatBedrock instance configured with the model
|
||||
"""
|
||||
MODEL_ARNS = {
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/global.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"meta.llama4-maverick-17b-instruct-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.meta.llama4-maverick-17b-instruct-v1:0",
|
||||
"meta.llama4-scout-17b-instruct-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.meta.llama4-scout-17b-instruct-v1:0",
|
||||
"amazon.nova-lite-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.amazon.nova-lite-v1:0",
|
||||
"amazon.nova-pro-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/us.amazon.nova-pro-v1:0",
|
||||
"amazon.nova-2-lite-v1:0": f"arn:aws:bedrock:{REGION}:{AWS_ACCOUNT}:inference-profile/global.amazon.nova-2-lite-v1:0",
|
||||
}
|
||||
PROVIDER = {
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": "anthropic",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": "anthropic",
|
||||
"meta.llama4-maverick-17b-instruct-v1:0": "meta",
|
||||
"meta.llama4-scout-17b-instruct-v1:0": "meta",
|
||||
"amazon.nova-lite-v1:0": "amazon",
|
||||
"amazon.nova-pro-v1:0": "amazon",
|
||||
"amazon.nova-2-lite-v1:0": "amazon",
|
||||
}
|
||||
prefix = {
|
||||
"anthropic.claude-haiku-4-5-20251001-v1:0": "us",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0": "global",
|
||||
"meta.llama4-maverick-17b-instruct-v1:0": "us",
|
||||
"meta.llama4-scout-17b-instruct-v1:0": "us",
|
||||
"amazon.nova-lite-v1:0": "us",
|
||||
"amazon.nova-pro-v1:0": "us",
|
||||
"amazon.nova-2-lite-v1:0": "global",
|
||||
}
|
||||
llm = ChatBedrockConverse(
|
||||
model_id=prefix[model_id] + "." + model_id,
|
||||
region_name=region,
|
||||
provider=PROVIDER[model_id],
|
||||
max_tokens=2048,
|
||||
temperature=0.7,
|
||||
)
|
||||
return llm.bind_tools(tools or [])
|
||||
|
||||
|
||||
def call_model(state: AgentState, llm) -> AgentState:
|
||||
"""Call the LLM with tools."""
|
||||
response = llm.invoke(state["messages"])
|
||||
state["current_step"] = "model_called"
|
||||
return {"messages": [response]}
|
||||
|
||||
|
||||
def call_tools(state: AgentState, tools_map: dict) -> AgentState:
|
||||
"""Execute any tool calls from the LLM response."""
|
||||
last_message = state["messages"][-1]
|
||||
|
||||
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
||||
tool_messages = []
|
||||
for tool_call in last_message.tool_calls:
|
||||
result = tools_map[tool_call["name"]].invoke(tool_call["args"])
|
||||
tool_messages.append(ToolMessage(content=str(result), tool_call_id=tool_call["id"]))
|
||||
|
||||
state["current_step"] = "tools_executed"
|
||||
return {"messages": tool_messages}
|
||||
else:
|
||||
state["current_step"] = "no_tools"
|
||||
return {"messages": []}
|
||||
|
||||
|
||||
def should_continue(state: AgentState) -> str:
|
||||
"""Determine if we should continue to tools or end."""
|
||||
last_message = state["messages"][-1]
|
||||
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
|
||||
return "tools"
|
||||
return "end"
|
||||
|
||||
|
||||
def create_agent(inference_profile_arn: str, region: str = REGION, tools: list = None):
|
||||
"""
|
||||
Create a LangGraph agent that uses Bedrock inference profile with tools.
|
||||
|
||||
Args:
|
||||
inference_profile_arn: ARN of the cross-region inference profile
|
||||
region: AWS region
|
||||
tools: List of LangChain tools to bind to the model
|
||||
|
||||
Returns:
|
||||
Compiled LangGraph workflow
|
||||
"""
|
||||
tools = tools or []
|
||||
llm = create_bedrock_llm(inference_profile_arn, region, tools)
|
||||
tools_map = {t.name: t for t in tools}
|
||||
|
||||
workflow = StateGraph(AgentState)
|
||||
workflow.add_node("model", lambda state: call_model(state, llm))
|
||||
workflow.add_node("tools", lambda state: call_tools(state, tools_map))
|
||||
workflow.set_entry_point("model")
|
||||
workflow.add_conditional_edges("model", should_continue, {"tools": "tools", "end": END})
|
||||
workflow.add_edge("tools", "model")
|
||||
|
||||
return workflow.compile()
|
||||
6
code/app/backend/config.py
Normal file
6
code/app/backend/config.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import os
|
||||
|
||||
TABLE = os.environ["TABLE"]
|
||||
REGION = os.environ["REGION"]
|
||||
AWS_ACCOUNT = os.environ["AWS_ACCOUNT"]
|
||||
SECRET_NAME = os.environ["SECRET_NAME"]
|
||||
53
code/app/backend/dynamo.py
Normal file
53
code/app/backend/dynamo.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import boto3
|
||||
import json
|
||||
import os
|
||||
from botocore.exceptions import ClientError
|
||||
from langfuse import Langfuse
|
||||
|
||||
from .config import REGION, TABLE, SECRET_NAME
|
||||
|
||||
dynamodb = boto3.resource("dynamodb", region_name=REGION)
|
||||
|
||||
|
||||
def get_secret() -> str:
|
||||
session = boto3.session.Session()
|
||||
client = session.client(service_name="secretsmanager", region_name=REGION)
|
||||
try:
|
||||
response = client.get_secret_value(SecretId=SECRET_NAME)
|
||||
except ClientError as e:
|
||||
raise e
|
||||
return response["SecretString"]
|
||||
|
||||
|
||||
secrets = json.loads(get_secret())
|
||||
langfuse = Langfuse(
|
||||
public_key=secrets["LANGFUSE-PUBLIC-KEY"],
|
||||
secret_key=secrets["LANGFUSE-SECRET-KEY"],
|
||||
host=os.environ["LANGFUSE_HOST"],
|
||||
)
|
||||
|
||||
|
||||
def get_contexto(dashboard: str) -> dict:
|
||||
"""
|
||||
Get contexto, filter, and items_disponiveis from DynamoDB for a given dashboard.
|
||||
|
||||
Returns:
|
||||
Dict with 'contexto', 'filter', and 'items_disponiveis' keys
|
||||
"""
|
||||
try:
|
||||
table = dynamodb.Table(TABLE)
|
||||
response = table.get_item(Key={"id": dashboard + "_contexto"})
|
||||
|
||||
if "Item" not in response:
|
||||
return {"contexto": "", "filter": "", "items_disponiveis": {}}
|
||||
|
||||
item = response["Item"]
|
||||
return {
|
||||
"contexto": item.get("contexto", ""),
|
||||
"filter": item.get("filter_key", ""),
|
||||
"items_disponiveis": item.get("itens_disponiveis", {}),
|
||||
}
|
||||
|
||||
except ClientError as e:
|
||||
error_message = e.response["Error"]["Message"]
|
||||
return {"contexto": f"Error: {error_message}", "filter": "", "items_disponiveis": {}}
|
||||
96
code/app/backend/orquestrador.py
Normal file
96
code/app/backend/orquestrador.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
|
||||
from langfuse.langchain import CallbackHandler
|
||||
|
||||
from .config import REGION
|
||||
from .dynamo import langfuse, get_contexto
|
||||
from .agent_bedrock import create_agent
|
||||
from .tools import ReportTools
|
||||
|
||||
|
||||
def main(user_query, history, model, base):
|
||||
"""Main execution function."""
|
||||
contexto_data = get_contexto(base)
|
||||
|
||||
id_mapping = {
|
||||
f"id_{i}": real_id
|
||||
for i, real_id in enumerate(contexto_data["items_disponiveis"], 1)
|
||||
}
|
||||
local_items = {
|
||||
local_id: contexto_data["items_disponiveis"][real_id]
|
||||
for local_id, real_id in id_mapping.items()
|
||||
}
|
||||
report_tools = ReportTools(id_mapping)
|
||||
|
||||
if contexto_data["filter"] == "period":
|
||||
CONSULT_RULES = """To use the tools you must give the id of the correspondant data, which can be associated to a given month and year in the following format year_month, which:
|
||||
-Year is the year in 4 digits (2025,2024,2023,2022,2021,...)
|
||||
-Month is th two digit representation: 01,02,03,04,05,06,07,08,09,10,11,12
|
||||
The format of the dict is: {id1:year_month1,id2:year_month2...}
|
||||
Choose the correct id based on the following dict:
|
||||
"""
|
||||
elif contexto_data["filter"] == "event":
|
||||
CONSULT_RULES = """To use the tools you must give the id of the correspondant data, which can be associated to a event, which is in the format "Name - City DD/MM/YYYY", where the last is a date in the format day/month/year. Theformat of elements in dict is {id1:event_description1,id2:event_description2...}"""
|
||||
else:
|
||||
CONSULT_RULES = """Wrong filter value, you must terminate the workflow and ask the user to contact the technical team"""
|
||||
|
||||
SYSTEM_PROMPT = """ You are a analitical agent in Brazilian Portuguese, with acess to monthly reports about a specific company, specified in the context. You have access to tools that lets you consult present variables in table, you always have access to "context", which keeps inside answers to different questions, that you may consult as you desire.
|
||||
Do not access other variables besides the ones reported by the tool and "context".
|
||||
You currently have access to data in a period specified in the context, so only answer questions inside the time window.
|
||||
<context>
|
||||
""" + contexto_data["contexto"] + """
|
||||
</context>
|
||||
""" + CONSULT_RULES + """
|
||||
<correlation>
|
||||
""" + str(local_items) + """
|
||||
</correlation>
|
||||
Here is the chat history:""" + history + """
|
||||
Inside the "NPS" in data is some useful values to calculate the NPS, which includes "distribuicao".
|
||||
Inside of it are grades and the amount of people who given that grade.
|
||||
Grades from 0 to 6 are detractors.
|
||||
Grades from 7 to 8 are neutral.
|
||||
Grades from 9 to 10 are promoters.
|
||||
Calculate the percentage of them when prompted about NPS and then calculate the nps using the following formula: NPS = %promoter - %detractor, never use the medium of the notes.
|
||||
|
||||
You have access to the tools:
|
||||
-get_consolidated_keys: Given a id returns the column names inside of a entity of a given table element.
|
||||
- get_monthly_report: given a id and a variable name, either one listed in the previous tool output or "context", returns its value. Using "context" gives you a summarization of many answers of questions asked to the customers.
|
||||
Answer, in Brazilian Portuguese, to the user the best you can with the given information, if you don't know the answer or how to answer say so, only answer from what you know.
|
||||
Always consult the most recent information when a date is not given, like questions "Quanto é meu nps?" """
|
||||
|
||||
langfuse_handler = CallbackHandler()
|
||||
agent = create_agent(model, REGION, tools=report_tools.as_tools())
|
||||
|
||||
initial_state = {
|
||||
"messages": [
|
||||
SystemMessage(content=SYSTEM_PROMPT),
|
||||
HumanMessage(content=user_query),
|
||||
],
|
||||
"current_step": "init",
|
||||
}
|
||||
|
||||
config = {"callbacks": [langfuse_handler], "tags": [base]}
|
||||
final_state = agent.invoke(initial_state, config=config)
|
||||
|
||||
total_input_tokens = 0
|
||||
total_output_tokens = 0
|
||||
for msg in final_state["messages"]:
|
||||
if isinstance(msg, AIMessage) and hasattr(msg, "usage_metadata") and msg.usage_metadata:
|
||||
total_input_tokens += msg.usage_metadata.get("input_tokens", 0)
|
||||
total_output_tokens += msg.usage_metadata.get("output_tokens", 0)
|
||||
|
||||
langfuse.flush()
|
||||
return {
|
||||
"response": final_state["messages"][-1].content,
|
||||
"input_tokens": total_input_tokens,
|
||||
"output_tokens": total_output_tokens,
|
||||
"total_tokens": total_input_tokens + total_output_tokens,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(
|
||||
"Liste o nps mês a mês desde maio 2025 até dezembro 2025",
|
||||
"",
|
||||
"anthropic.claude-sonnet-4-5-20250929-v1:0",
|
||||
"bacio_transacional_loja_app",
|
||||
)
|
||||
85
code/app/backend/tools.py
Normal file
85
code/app/backend/tools.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from botocore.exceptions import ClientError
|
||||
from langchain_core.tools import StructuredTool
|
||||
|
||||
from .config import TABLE
|
||||
from .dynamo import dynamodb
|
||||
|
||||
|
||||
class ReportTools:
|
||||
def __init__(self, id_mapping: dict[str, str]):
|
||||
self.id_mapping = id_mapping
|
||||
|
||||
def get_variable_value(self, id: str, variable: str) -> str:
|
||||
"""
|
||||
Get a specific variable's value from DynamoDB for a specific id.
|
||||
|
||||
Args:
|
||||
id: The id of the data
|
||||
variable: The variable/column name to retrieve from the table
|
||||
|
||||
Returns:
|
||||
The content of the specified variable for the given id
|
||||
"""
|
||||
real_id = self.id_mapping.get(id, id)
|
||||
try:
|
||||
table = dynamodb.Table(TABLE)
|
||||
response = table.get_item(Key={"id": real_id})
|
||||
|
||||
if "Item" not in response:
|
||||
return f"No report found for month: {id}"
|
||||
|
||||
item = response["Item"]
|
||||
content = item.get(variable, "")
|
||||
|
||||
if not content:
|
||||
return f"Variable '{variable}' not found for month: {id}"
|
||||
|
||||
return f"<{id}>\n{content}\n</{id}>"
|
||||
|
||||
except ClientError as e:
|
||||
error_message = e.response["Error"]["Message"]
|
||||
return f"Error fetching report: {error_message}"
|
||||
|
||||
def get_variables_list(self, id: str) -> str:
|
||||
"""
|
||||
Get the list of variables available in the table for a specific month.
|
||||
|
||||
Args:
|
||||
id: The id of the data
|
||||
|
||||
Returns:
|
||||
The list of available variables/keys for the specified data
|
||||
"""
|
||||
real_id = self.id_mapping.get(id, id)
|
||||
try:
|
||||
table = dynamodb.Table(TABLE)
|
||||
response = table.get_item(Key={"id": real_id})
|
||||
|
||||
if "Item" not in response:
|
||||
return f"No data found for month: {id}"
|
||||
|
||||
item = response["Item"]
|
||||
chaves_consolidadas = item.get("chaves_consolidadas", "")
|
||||
|
||||
if not chaves_consolidadas:
|
||||
return f"No consolidated keys found for id: {id}"
|
||||
|
||||
return chaves_consolidadas
|
||||
|
||||
except ClientError as e:
|
||||
error_message = e.response["Error"]["Message"]
|
||||
return f"Error fetching consolidated keys: {error_message}"
|
||||
|
||||
def as_tools(self) -> list:
|
||||
return [
|
||||
StructuredTool.from_function(
|
||||
self.get_variable_value,
|
||||
name="get_variable_value",
|
||||
description="Get a specific variable's data from DynamoDB for a specific id.",
|
||||
),
|
||||
StructuredTool.from_function(
|
||||
self.get_variables_list,
|
||||
name="get_variable_list",
|
||||
description="Get the list of variables available in the table for a specific id.",
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
import streamlit as st
|
||||
import 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
"""
|
||||
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 remove_xml_declaration(content: str) -> str:
|
||||
"""
|
||||
Remove only the XML declaration from content.
|
||||
|
||||
Args:
|
||||
content: Raw XML content
|
||||
|
||||
Returns:
|
||||
Content without XML declaration (keeps relatorio tags)
|
||||
"""
|
||||
content = re.sub(r'<\?xml[^?]*\?>\s*', '', content)
|
||||
return content.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' and 'dados_consolidados' fields are 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", "")
|
||||
dados_consolidados = item.get("dados_consolidados", "")
|
||||
|
||||
# Clean the XML content
|
||||
cleaned_context = clean_context_xml(context)
|
||||
cleaned_dados = remove_xml_declaration(dados_consolidados)
|
||||
|
||||
xml_parts.append(f"<{period}>")
|
||||
xml_parts.append(cleaned_context)
|
||||
if cleaned_dados:
|
||||
xml_parts.append(cleaned_dados)
|
||||
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"))
|
||||
Reference in New Issue
Block a user