Artigo
· Abr. 18 18min de leitura

Comandando a Tripulação

Image generated by OpenAI DALL·E

Sou um grande fã de ficção científica, mas embora eu esteja totalmente a bordo da nave Star Wars (desculpas aos meus colegas Trekkies!), sempre apreciei os episódios clássicos de Star Trek da minha infância.
A tripulação diversificada da USS Enterprise, cada um dominando suas funções únicas, é uma metáfora perfeita para entender os agentes de IA e seu poder em projetos como o Facilis.
Então, vamos embarcar em uma missão intergaláctica, utilizando a IA como a tripulação da nossa nave e  audaciosamente ir audaciosamente ir homem jamais esteve
Esse conceito de trabalho em equipe é uma analogia maravilhosa para ilustrar como os agentes de IA funcionam e como os usamos em nosso projeto DC-Facilis. Então, vamos mergulhar e assumir o papel de um capitão de nave estelar, liderando uma tripulação de IA em territórios inexplorados!

Bem-vindo ao CrewAI!

Para gerenciar nossa tripulação de IA, usamos uma estrutura fantástica chamada CrewAI.É enxuta, extremamente rápida e opera como uma plataforma Python multiagente. Uma das razões pelas quais a amamos, além do fato de ter sido criada por outro brasileiro, é sua incrível flexibilidade e design baseado em funções.

from crewai import Agent, Task, Crew

the taken quote

Conheça os Planejadores

No Facilis, nossos agentes de IA são divididos em dois grupos. Vamos começar com o primeiro, que gosto de chamar de "Os Planejadores".

O Agente de Extração

O papel principal do Facilis é receber uma descrição em linguagem natural de um serviço REST e criar automaticamente toda a interoperabilidade necessária. Portanto, nosso primeiro membro da tripulação é o Agente de Extração. Este agente tem a tarefa de "extrair" as especificações da API a partir da descrição fornecida pelo usuário.

Aqui está o que o Agente de Extração procura:

  • Host (obrigatório)
  • Endpoint (obrigatório)
  • Params (opcional)
  • Port (se disponível)
  • modelo JSON (para POST/PUT/PATCH/DELETE)
  • Autenticação (se aplicável)
    def create_extraction_agent(self) -> Agent:
        return Agent(
            role='Extrator de Especificações de API',
            goal='Extrair especificações de API de descrições em linguagem natural',
            backstory=dedent("""
                        Você é especializado em interpretar descrições em linguagem natural
                        e extrair especificações de API estruturadas.
            """),
            allow_delegation=True,
            llm=self.llm
        )

    def extract_api_specs(self, descriptions: List[str]) -> Task:
        return Task(
            description=dedent(f"""
               Extraia as especificações de API das seguintes descrições:
                {json.dumps(descriptions, indent=2)}

               Para cada descrição, extraia:
                - Host (obrigatório)
                - Endpoint (obrigatório)
                - Params (opcional)
                - Port (se disponível)
                - modelo JSON (para POST/PUT/PATCH/DELETE)
                - Autenticação (se aplicável)

                Marque quaisquer campos obrigatórios ausentes como 'missing'.
                Retorne os resultados em formato JSON como um array de especificações.
            """),
            expected_output="""Um array JSON contendo as especificações de API extraídas com todos os campos obrigatórios e opcionais""",
            agent=self.extraction_agent
        )

O Agente de Validação

Próximo na fila, o Agente de Validação! Sua missão é garantir que as especificações de API coletadas pelo Agente de Extração estejam corretas e consistentes. Ele verifica:

  1. Formato de host válido
  2. Endpoint começando com '/'
  3. Métodos HTTP válidos (GET, POST, PUT, DELETE, PATCH)
  4. Número de porta válido (se fornecido)
  5. Presença de modelo JSON para métodos aplicáveis.

def create_validation_agent(self) -> Agent: return Agent( role='API Validator', goal='Validar especificações de API quanto à correção e consistência.', backstory=dedent(""" Você é um especialista em validação de API, garantindo que todas as especificações atendam aos padrões e formatos necessários. """), allow_delegation=False, llm=self.llm ) def validate_api_spec(self, extracted_data: Dict) -> Task: return Task( description=dedent(f""" Valide a seguinte especificação de API: {json.dumps(extracted_data, indent=2)} Verifique: 1. Formato de host válido 2. Endpoint começando com '/' 3. Métodos HTTP válidos (GET, POST, PUT, DELETE, PATCH) 4. Número de porta válido (se fornecido) 5. Presença de modelo JSON para métodos aplicáveis. Retorne os resultados da validação em formato JSON. """), expected_output="""Um objeto JSON contendo os resultados da validação com quaisquer erros ou confirmação de validade""", agent=self.validation_agent )

O Agente de Interação

Avançando, conhecemos o Agente de Interação, nosso Especialista em Interação com o Usuário. Seu papel é obter quaisquer campos de especificação de API ausentes que foram marcados pelo Agente de Extração e validá-los com base nas descobertas do Agente de Validação. Eles interagem diretamente com os usuários para preencher quaisquer lacunas.

O Agente de Produção

Precisamos de duas informações cruciais para criar a interoperabilidade necessária: namespace e nome da produção. O Agente de Produção interage com os usuários para coletar essas informações, de forma muito semelhante ao Agente de Interação.

O Agente de Transformação de Documentação

Assim que as especificações estiverem prontas, é hora de convertê-las em documentação OpenAPI. O Agente de Transformação de Documentação, um especialista em OpenAPI, cuida disso.

    def create_transformation_agent(self) -> Agent:
        return Agent(
            role='Especialista em Transformação OpenAPI',
            goal='Converter especificações de API em documentação OpenAPI',
            backstory=dedent("""
                Você é um especialista em especificações e documentação OpenAPI.
                Seu papel é transformar detalhes de API validados em documentação
                OpenAPI 3.0 precisa e abrangente.
            """),
            allow_delegation=False,
            llm=self.llm
        )

    def transform_to_openapi(self, validated_endpoints: List[Dict], production_info: Dict) -> Task:
        return Task(
            description=dedent(f"""
                Transforme as seguintes especificações de API validadas em documentação OpenAPI 3.0:

                Informações de Produção:
                {json.dumps(production_info, indent=2)}

               Endpoints Validados:
                {json.dumps(validated_endpoints, indent=2)}

               Requisitos:
               1. Gerar especificação OpenAPI 3.0 completa
               2. Incluir schemas de requisição/resposta apropriados
               3. Documentar todos os parâmetros e corpos de requisição
               4. Incluir autenticação se especificado
               5. Garantir formatação de caminho apropriada

                Retorne a especificação OpenAPI nos formatos JSON e YAML.
            """),
            expected_output="""Um objeto JSON contendo a especificação OpenAPI 3.0 completa com todos os endpoints e schemas.""",
            agent=self.transformation_agent
        )

The Review Agent

Após a transformação, a documentação OpenAPI passa por uma revisão meticulosa para garantir conformidade e qualidade. O Agente de Revisão segue esta lista de verificação:

1.Conformidade OpenAPI 3.0
- Especificação de versão correta
- Elementos raiz obrigatórios
- Validação da estrutura do schema
2. Completude
- Todos os endpoints documentados
- Parâmetros totalmente especificados
- Schemas de requisição/resposta definidos
- Esquemas de segurança configurados corretamente
3. Verificações de Qualidade
- Convenções de nomenclatura consistentes
- Descrições claras
- Uso adequado de tipos de dados
- Códigos de resposta significativos
4. Melhores Práticas
- Uso adequado de tags
- Nomenclatura de parâmetros consistente
- Definições de segurança apropriadas

Finalmente, se tudo parecer bom, o Agente de Revisão reporta um objeto JSON saudável com a seguinte estrutura:

{
 "is_valid": boolean,
 "approved_spec": object (a especificação OpenAPI revisada e possivelmente corrigida),
 "issues": [array de strings descrevendo quaisquer problemas encontrados],
 "recommendations": [array de sugestões de melhoria]
}

def create_reviewer_agent(self) -> Agent: return Agent( role='Revisor de Documentação OpenAPI', goal='Garantir a conformidade e qualidade da documentação OpenAPI', backstory=dedent(""" Você é a autoridade final em qualidade e conformidade de documentação OpenAPI. Com vasta experiência em especificações OpenAPI 3.0, você revisa meticulosamente a documentação para precisão, completude e adesão aos padrões. """), allow_delegation=True, llm=self.llm ) def review_openapi_spec(self, openapi_spec: Dict) -> Task: return Task( description=dedent(f""" Revise a seguinte especificação OpenAPI para conformidade e qualidade: {json.dumps(openapi_spec, indent=2)} Lista de Verificação da Revisão:: 1. Conformidade OpenAPI 3.0 - Verificar a especificação de versão correta - Verificar os elementos raiz obrigatórios - Validar a estrutura do schema 2. Completude - Todos os endpoints devidamente documentados - Parâmetros totalmente especificados - Schemas de requisição/resposta definidos - Esquemas de segurança configurados corretamente 3. Quality Checks - Consistent naming conventions - Clear descriptions - Proper use of data types - Meaningful response codes 4. Best Practices - Proper tag usage - Consistent parameter naming - Appropriate security definitions Você deve retornar um objeto JSON com a seguinte estrutura: {{ "is_valid": boolean, "approved_spec": object (a especificação OpenAPI revisada e possivelmente corrigida), "issues": [array de strings descrevendo quaisquer problemas encontrados], "recommendations": [array de sugestões de melhoria] }} """), expected_output=""" Um objeto JSON contendo: is_valid (boolean), approved_spec (object), issues (array), e recommendations (array)""", agent=self.reviewer_agent )

O Agente Iris

O último agente no grupo do planejador é o Agente Iris, que envia a documentação OpenAPI finalizada para o Iris.


def create_iris_i14y_agent(self) -> Agent: return Agent( role='Especialista em Integração Iris I14y', goal='Integrar especificações de API com o serviço Iris I14y', backstory=dedent(""" Você é responsável por garantir uma integração suave entre o sistema de documentação da API e o serviço Iris I14y. Você lida com a comunicação com o Iris, valida as respostas e garante a integração bem-sucedida das especificações da API. """), allow_delegation=False, llm=self.llm ) def send_to_iris(self, openapi_spec: Dict, production_info: Dict, review_result: Dict) -> Task: return Task( description=dedent(f""" Enviar a especificação OpenAPI aprovada para o serviço Iris I14y: Informações de Produção: - Nome: {production_info['production_name']} - Namespace: {production_info['namespace']} - É Novo: {production_info.get('create_new', False)} Status da Revisão: - Aprovado: {review_result['is_valid']} Retornar o resultado da integração em formato JSON. """), expected_output="""Um objeto JSON contendo o resultado da integração com o serviço Iris I14y, incluindo o status de sucesso e os detalhes da resposta.""", agent=self.iris_i14y_agent ) class IrisI14yService: def __init__(self): self.logger = logging.getLogger('facilis.IrisI14yService') self.base_url = os.getenv("FACILIS_URL", "http://dc-facilis-iris-1:52773") self.headers = { "Content-Type": "application/json" } self.timeout = int(os.getenv("IRIS_TIMEOUT", "504")) # in milliseconds self.max_retries = int(os.getenv("IRIS_MAX_RETRIES", "3")) self.logger.info("IrisI14yService initialized") async def send_to_iris_async(self, payload: Dict) -> Dict: """ Enviar carga útil para o endpoint de geração do Iris de forma assíncrona. """ self.logger.info("Enviando carga útil para o endpoint de geração do Iris.") if isinstance(payload, str): try: json.loads(payload) except json.JSONDecodeError: raise ValueError("Invalid JSON string provided") retry_count = 0 last_error = None # Cria timeout para o aiohttp timeout = aiohttp.ClientTimeout(total=self.timeout / 1000) # Converte ms para seconds while retry_count < self.max_retries: try: self.logger.info(f"Attempt {retry_count + 1}/{self.max_retries}: Enviando requisição para {self.base_url}/facilis/api/generate") async with aiohttp.ClientSession(timeout=timeout) as session: async with session.post( f"{self.base_url}/facilis/api/generate", json=payload, headers=self.headers ) as response: if response.status == 200: return await response.json() response.raise_for_status() except asyncio.TimeoutError as e: retry_count += 1 last_error = e error_msg = f"Timeout occurred (attempt {retry_count}/{self.max_retries})" self.logger.warning(error_msg) if retry_count < self.max_retries: wait_time = 2 ** (retry_count - 1) self.logger.info(f"Waiting {wait_time} seconds before retry...") await asyncio.sleep(wait_time) continue except aiohttp.ClientError as e: error_msg = f"Failed to send to Iris: {str(e)}" self.logger.error(error_msg) raise IrisIntegrationError(error_msg) error_msg = f"Failed to send to Iris after {self.max_retries} attempts due to timeout" self.logger.error(error_msg) raise IrisIntegrationError(error_msg, last_error)

Conheça os Geradores

Nosso segundo conjunto de agentes são os Geradores. Eles estão aqui para transformar as especificações OpenAPI em interoperabilidade InterSystems IRIS.
Há oito deles neste grupo.

O primeiro deles é o Agente Analisador. Ele é como o planejador, traçando a rota.
Seu trabalho é mergulhar nas especificações OpenAPI e descobrir quais componentes de Interoperabilidade IRIS são necessários.


def create_analyzer_agent(): return Agent( role="Analisador de Especificações OpenAPI", goal="Analisar minuciosamente as especificações OpenAPI e planejar os componentes de Interoperabilidade IRIS", backstory="""Você é um especialista em especificações OpenAPI e em Interoperabilidade InterSystems IRIS. Seu trabalho é analisar documentos OpenAPI e criar um plano detalhado de como eles devem ser implementados como componentes de Interoperabilidade IRIS.""", verbose=False, allow_delegation=False, tools=[analyze_openapi_tool], llm=get_facilis_llm() ) analysis_task = Task( description="""Analisar a especificação OpenAPI e planejar os componentes de Interoperabilidade IRIS necessários. Incluir uma lista de todos os componentes que devem estar na classe de Produção."", agent=analyzer, expected_output="Uma análise detalhada da especificação OpenAPI e um plano para os componentes IRIS, incluindo a lista de componentes de Produção", input={ "openapi_spec": openApiSpec, "production_name": "${production_name}" } )

Em seguida, os Agentes de Business Services (BS) e Business Operations (BO) assumem o controle.
Eles geram os Business Services e as Business Operations com base nos endpoints OpenAPI.
Eles usam uma ferramenta útil chamada MessageClassTool para gerar as classes de mensagens perfeitas, garantindo a comunicação.


def create_bs_generator_agent(): return Agent( role="Gerador de Produção e Business Service IRIS", goal="Gerar classes de Produção e Business Service IRIS formatadas corretamente a partir de especificações OpenAPI", backstory="""Você é um desenvolvedor InterSystems IRIS experiente, especializado em Produções de Interoperabilidade. Sua expertise reside na criação de Business Services e Produções capazes de receber e processar requisições de entrada com base emespecificações de API."", verbose=False, allow_delegation=True, tools=[generate_production_class_tool, generate_business_service_tool], llm=get_facilis_llm() ) def create_bo_generator_agent(): return Agent( role="Gerador de Business Operation IRIS", goal="Gerar classes de Business Operation IRIS formatadas corretamente a partir de especificações OpenAPI", backstory="""Você é um desenvolvedor InterSystems IRIS experiente, especializado em Produções de Interoperabilidade. Sua expertise reside na criação de Business Operations capazes de enviar requisições para sistemas externos com base em especificações de API.""", verbose=False, allow_delegation=True, tools=[generate_business_operation_tool, generate_message_class_tool], llm=get_facilis_llm() ) bs_generation_task = Task( description="Gerar classes de Business Service com base nos endpoints OpenAPI", agent=bs_generator, expected_output="Definições de classes de Business Service do IRIS", context=[analysis_task] ) bo_generation_task = Task( description="Gerar classes de Business Operation com base nos endpoints OpenAPI", agent=bo_generator, expected_output="Definições de classes de Business Operation do IRIS", context=[analysis_task] ) class GenerateMessageClassTool(BaseTool): name: str = "generate_message_class" description: str = "Gerar uma classe de Mensagem IRIS" input_schema: Type[BaseModel] = GenerateMessageClassToolInput def _run(self, message_name: str, schema_info: Union[str, Dict[str, Any]]) -> str: writer = IRISClassWriter() try: if isinstance(schema_info, str): try: schema_dict = json.loads(schema_info) except json.JSONDecodeError: return "Error: Invalid JSON format for schema info" else: schema_dict = schema_info class_content = writer.write_message_class(message_name, schema_dict) # Armazenar a classe gerada. writer.generated_classes[f"MSG.{message_name}"] = class_content return class_content except Exception as e: return f"Error generating message class: {str(e)}"

Depois que BS e BO fazem o que têm que fazer, é a hora do Agente de Produção brilhar!
Este agente junta tudo para criar um ambiente de produção coeso.

Depois que tudo estiver configurado, o próximo na linha é o Agente de Validação.
Este entra em cena para uma verificação final, garantindo que cada classe Iris esteja ok.

Em seguida, temos o Agente de Exportação e o Agente de Coleção. O Agente de Exportação gera os arquivos .cls, enquanto o Agente de Coleção reúne todos os nomes de arquivos.
Tudo é passado para o importador, que compila tudo no InterSystems Iris.


def create_exporter_agent(): return Agent( role="Exportador de Classes IRIS", goal="Exportar e validar definições de classes IRIS para arquivos .cls adequados", backstory="""Você é um especialista em implantação InterSystems IRIS. Seu trabalho é garantir que as definições de classes IRIS geradas sejam devidamente exportadas como arquivos .cls válidos que possam ser importados diretamente para um ambiente IRIS.""", verbose=False, allow_delegation=False, tools=[export_iris_classes_tool, validate_iris_classes_tool], llm=get_facilis_llm() ) def create_collector_agent(): return Agent( role="Coletor de Classes IRIS", goal="Coletar todos os arquivos de classes IRIS gerados em uma coleção JSON", backstory="""Você é um especialista em sistema de arquivos responsável por reunir e organizar os arquivos de classes IRIS gerados em uma coleção estruturada.""", verbose=False, allow_delegation=False, tools=[CollectGeneratedFilesTool()], llm=get_facilis_llm() ) export_task = Task( description="Exportar todas as classes IRIS geradas como arquivos .cls válidos", agent=exporter, expected_output="Arquivos .cls IRIS válidos salvos no diretório de saída", context=[bs_generation_task, bo_generation_task], input={ "output_dir": "/home/irisowner/dev/output/iris_classes" # Optional } ) collection_task = Task( description="Coletar todos os arquivos de classes IRIS gerados em uma coleção JSON", agent=collector, expected_output="Coleção JSON de todos os arquivos .cls gerados", context=[export_task, validate_task], input={ "directory": "./output/iris_classes" } )

Limitações e Desafios

Nosso projeto começou como um experimento empolgante, onde meus colegas mosqueteiros e eu almejávamos criar uma ferramenta totalmente automatizada usando agentes.
Foi uma jornada selvagem!
Nosso foco principal estava em integrações de API REST. É sempre uma alegria receber uma tarefa com uma especificação OpenAPI para integrar; no entanto, sistemas legados podem ser uma história completamente diferente.
Pensamos que automatizar essas tarefas poderia ser incrivelmente útil.
Mas toda aventura tem suas reviravoltas:
Um dos maiores desafios foi instruir a IA a converter OpenAPI para Interoperabilidade Iris.
Começamos com o modelo openAI GPT3.5-turbo, que em testes iniciais se mostrou difícil com depuração e prevenção de interrupções.
A mudança para o Anthropic Claude 3.7 Sonnet mostrou melhores resultados para o grupo Gerador, mas não tanto para os Planejadores...
Isso nos levou a dividir nossas configurações de ambiente, usando diferentes provedores de LLM para flexibilidade.
Usamos GPT3.5-turbo para planejamento e Claude sonnet para geração, uma ótima combinação!
Essa combinação funcionou bem, mas encontramos problemas com alucinações.
A mudança para o GT4o melhorou os resultados, mas ainda enfrentamos alucinações na criação de classes Iris e, às vezes, especificações OpenAPI desnecessárias, como o renomado exemplo Pet Store OpenAPI.
Nos divertimos muito aprendendo ao longo do caminho, e estou super animado com o futuro incrível nesta área, com inúmeras possibilidades!

Discussão (1)1
Entre ou crie uma conta para continuar