Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat static multi agent #110

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions app/endpoints/multiagents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
from fastapi import APIRouter, Request, Security
import asyncio

from app.schemas.multiagents import MultiAgentsRequest
from app.helpers import LanguageModelReranker
from app.schemas.security import User
from app.utils.settings import settings
from app.utils.lifespan import clients, limiter
from app.utils.security import check_api_key, check_rate_limit


router = APIRouter()

explain_choice = {
0: "Je ne comprends pas la demande.",
1: "Des informations pertinentes ont été trouvées dans la base de données cherchée.",
2: "Je n'ai pas trouvé d'informations pertinentes en base de données, mais il me semblait juste de répondre avec mes connaissances générales.",
3: "Je n'ai pas trouvé d'informations pertinentes en base de données, et je ne veux pas me mouiller en répondant quelque chose de faux.",
4: "La décision d'aller sur internet a été prise pour chercher des informations pertinentes.",
}


def prep_net(body, user):
body.collections = []
internet_chunks = []
internet_chunks = clients.internet.get_chunks(prompt=body.prompt)
internet_collection = clients.internet.create_temporary_internet_collection(internet_chunks, body.collections, user)
body.collections.append(internet_collection.id)


def get_prompt_teller_multi(question, docs_tmp, choice):
"""
Create a batch of prompts based on docs for the writers maker LLMs.
If there is no docs (no context needed), only create one prompt.
"""
prompts = []
if choice == 1 or choice == 4:
for doc in docs_tmp:
prompt_teller = f"""
Tu es un assistant administratif qui réponds a des questions sur le droit et l'administratif en Français. Tes réponses doit être succinctes et claires. Ne détailles pas inutilement.
Voilà un contexte : \n{doc}\n
Voilà une question : {question}
En ne te basant uniquement sur le contexte donné, réponds à la question avec une réponse de la meilleure qualité possible.
- Si le contexte ne te permets pas de répondre à la question, réponds juste "Rien ici", ne dis jamais "le texte ne mentionne pas".
- Si le contexte donne des éléments de réponse, réponds uniquement a la question et n'inventes rien, donnes même juste quelques éléments de réponse si tu n'arrives pas à répondre totalement avec le contexte. Donnes le nom du texte du contexte dans ta réponse.
- Si la question n'est pas explicite et renvoie à la conversation en cours, et que tu trouve que le contexte est en lien avec la conversation, réponds juste "Ces informations sont interessantes pour la conversation".
question : {question}
réponse ("Rien ici" ou ta réponse):
"""
prompts.append(prompt_teller)
elif choice == 2:
for i in range(1):
prompt_teller = f"""
Tu es un assistant administratif qui réponds a des questions sur le droit et l'administratif en Français. Nous sommes en 2024. Tes réponses doit être succinctes et claires. Ne détailles pas inutilement.
Voilà une demande utilisateur : {question}
Réponds à cette question comme tu peux.
Règles à respecter :
N'inventes pas de référence.
Si tu as besoin de plus d'information ou que la question n'est pas claire, dis le a l'utilisateur.
La réponse doit être la plus courte possible. Mets en forme ta réponse avec des sauts de lignes. Réponds en Français et part du principe que l'interlocuteur est Français et que ses questions concerne la France.
Réponse :
"""
prompts.append(prompt_teller)
return prompts


def get_completion(model, prompt, user, temperature=0.2, max_tokens=200):
response = clients.models[model].chat.completions.create(
messages=[{"role": "user", "content": prompt}],
model=model, # self.model_clients[model],
temperature=temperature,
max_tokens=max_tokens,
stream=False,
user=user,
)
result = response.choices[0].message.content
return result


async def get_completion_async(model, prompt, user, temperature, max_tokens):
return get_completion(model, prompt, user, temperature, max_tokens)


async def ask_in_parallel(model, prompts, user):
tasks = []
for prompt in prompts:
print("-" * 32)
print(prompt)
print("-" * 32)
task = asyncio.create_task(get_completion_async(model, prompt, user, temperature=0.2, max_tokens=200))
tasks.append(task)
answers = await asyncio.gather(*tasks)
return answers


PROMPT_CONCAT = """
Tu es un expert pour rédiger les bonnes réponses et expliquer les choses.
Voila plusieurs réponses générées par des agents : {answers}
En te basant sur ces réponses, ne gardes que ce qui est utile pour répondre à la question : {prompt}
Cites les sources utilisées s'il y en a, mais ne parle jamais des "réponses des agents".
Réponds avec une réponse à cette question de la meilleure qualité possible.
Si des éléments de réponses sont contradictoire, donnes les quand même à l'utilisateur en expliquant les informations que tu as.
Réponds juste à la question, ne dis rien d'autre. Tu dois faire un mélange de ces informations pour ne sortir que l'utile de la meilleure manière possible. Termines ta réponse avec un emoji.
Réponse :
"""


def remove_duplicates(lst):
seen = set()
return [x for x in lst if not (x in seen or seen.add(x))]


@router.post("/multiagents")
@limiter.limit(settings.default_rate_limit, key_func=lambda request: check_rate_limit(request=request))
async def multiagents(request: Request, body: MultiAgentsRequest, user: User = Security(check_api_key)):
"""Multi Agents researcher."""

reranker = LanguageModelReranker(model=clients.models[body.supervisor_model])
client = clients.models[body.supervisor_model]
print("YOOOOO")
print(body)
url = f"{client.base_url}multiagents"
headers = {"Authorization": f"Bearer {client.api_key}"}

searches = clients.search.query(
prompt=body.prompt,
collection_ids=body.collections,
method=body.method,
k=25, # body.k,
rff_k=25, # body.rff_k,
score_threshold=body.score_threshold,
user=user,
)
initial_docs = [doc.chunk.content for doc in searches]
print(searches)
initial_refs = [doc.chunk.metadata.document_name for doc in searches]

async def go_multiagents(body, initial_docs, initial_refs, n_retry, max_retry=5, window=5):
docs_tmp = initial_docs[n_retry * window : (n_retry + 1) * window]
refs_tmp = initial_refs[n_retry * window : (n_retry + 1) * window]

input = ["(Extrait : " + ref + ") " + doc[:250] + "..." for doc, ref in zip(docs_tmp, refs_tmp)]
choice = reranker.create(prompt=body.prompt, input=input, type="choicer")[0].score

if choice in [0, 3] and n_retry < max_retry:
print(f"retry ! {n_retry}")
return await go_multiagents(body, initial_docs, initial_refs, n_retry=n_retry + 1, max_retry=5, window=5)
elif choice in [1, 2]:
print("yay 1 or 2")
elif choice == 4 or n_retry >= max_retry: # else ?
print("Internet time")
prep_net(body, user)
print("should be internet:", body.collections)
searches = clients.search.query(
prompt=body.prompt,
collection_ids=body.collections,
method=body.method,
k=5, # body.k,
rff_k=5, # body.rff_k,
score_threshold=body.score_threshold,
user=user,
)
docs_tmp = [doc.chunk.content for doc in searches]
refs_tmp = [doc.chunk.metadata.document_name for doc in searches]
prompts = get_prompt_teller_multi(body.prompt, docs_tmp, choice)
answers = await ask_in_parallel(body.writers_model, prompts, body.user)
prompt = PROMPT_CONCAT.format(prompt=body.prompt, answers=answers)
answer = get_completion(body.supervisor_model, prompt, body.user, temperature=0.2, max_tokens=600)
return answer, docs_tmp, refs_tmp, choice, n_retry

answer, docs_tmp, refs, choice, n_retry = await go_multiagents(body, initial_docs, initial_refs, n_retry=0, max_retry=5, window=5)

response = {}
response["answer"] = answer
response["choice"] = choice
response["choice_desc"] = explain_choice[choice]
response["n_retry"] = n_retry
response["sources_refs"] = remove_duplicates(refs)
response["sources_content"] = remove_duplicates(docs_tmp)
if choice == 2:
response["sources_refs"] = ["Trust me bro."]
response["sources_content"] = []
return response
87 changes: 77 additions & 10 deletions app/helpers/_languagemodelreranker.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,95 @@
from typing import List
import re
from typing import List, Literal

from app.clients._modelclients import ModelClient
from app.schemas.rerank import Rerank


class LanguageModelReranker:
PROMPT_LLM_BASED = """Voilà un texte : {text}\n
En se basant uniquement sur ce texte, réponds 1 si ce texte peut donner des éléments de réponse à la question suivante ou 0 si aucun élément de réponse n'est présent dans le texte. Voila la question: {prompt}
Le texte n'a pas besoin de répondre parfaitement à la question, juste d'apporter des éléments de réponses et/ou de parler du même thème. Réponds uniquement 0 ou 1."""
Le texte n'a pas besoin de répondre parfaitement à la question, juste d'apporter des éléments de réponses et/ou de parler du même thème. Réponds uniquement 0 ou 1.
"""

PROMPT_CHOICER = """
Tu es un expert en compréhension et en évaluation des besoins en information pour répondre à un message utilisateur. Ton travail est de juger la possibilité de répondre à un message utilisateur en fonction d'un contexte donné.
Nous sommes en 2024 et ton savoir s'arrete en 2023.

Le contexte est composé d'une liste d'extrait d'article qui sert d'aide pour répondre au message utilisateur, mais n'est pas forcément en lien avec lui. Tu dois évaluer s'il y a besoin du contexte ou non.

Ne réponds pas au message utilisateur.
Voilà le message utilisateur : {prompt}

Voilà tes choix :

- Si le message utilisateur n'est vraiment pas claire ou ne veut vraiment rien dire en français réponds 0 OU
- Si le message utilisateur est compréhensible et que le contexte donné est en lien avec le message utilisateur (même de loin, même un seul article du contexte) / Si le message utilisateur aborde un sujet qui est également abordé dans le contexte réponds 1 OU
- Si le contexte contient certains éléments qui peuvent aider à répondre au message utilisateur réponds 1 OU
- Si le message utilisateur demande explicitement des sources ou des références réponds 1 (si le contexte associé est bon) ou 3 (si le contexte associé est mauvais) OU
- Si le message utilisateur n'a pas besoin de contexte car ce n'est pas une question adminitrative / c'est de la culture générale simple réponds 2 OU
- Si le message utilisateur est un message simple ou personnel / Le reste de la conversation permets d'y répondre réponds 2 OU
- Si le message utilisateur a besoin de contexte car elle est spécifique, sur de l'administratif, ou complexe, mais qu'aucun des articles du contexte n'est en lien avec elle réponds 3
- Si on te demande de chercher sur internet / qu'on te demande des informations sur quelqu'un ou une personnalité / qu'on te demande des informations actuelles / si le message utilisateur commence par "internet" réponds 4

def __init__(self, model: ModelClient) -> None:
Pour chaque choix, assure-toi de bien évaluer le message utilisateur selon ces critères avant de donner ta réponse.
Regardes bien le contexte, s'il peut t'aider à répondre au message utilisateur c'est important.
Même si le contexte ne contient que quelques informations ou mots commun avec le message utilisateur, considère qu'il est en lien avec la question.

Ne fais pas de phrase, réponds uniquement 0, 1, 2, 3 ou 4.

Exemples
----------
Exemple 1 - "Le contexte permet de répondre à la question"
context : Pour la retraite anticipée [...]
question : Comment bien préparer sa retraite ?
reponse : 1
Exemple 2 - "toto voiture n'est pas une question et ne veut rien dire"
context : les assurances de véhicules [...]
question : toto voiture
reponse : 0
Exemple 3 : "Pas besoin de contexte, la question est de la culture générale / facile"
context : En cas de vol ou de perte [...]
question : Quelle est la capitale de la France ?
reponse : 2
Exemple 4 : "Question necessitant du contexte pertinent mais pas dans le rag"
context : Vous pouvez faire une demarche [...]
question : Qui est le président des usa actuellement ?
reponse : 4
----------

Ne réponds pas à la question, réponds uniquement 0, 1, 2, 3 ou 4. Ne donnes jamais d'explication ou de phrase dans ta réponse, renvoies juste un chiffre. Ta réponse doit être sous ce format:<CHIFFRE>
Bases toi également sur le reste des messages de la conversation pour répondre avec ton choix.
context : {text}
question : {prompt}
reponse :
"""

def __init__(self, model: ModelClient):
self.model = model

def create(self, prompt: str, input: list) -> List[Rerank]:
def create(self, prompt: str, input: list, type: Literal["basic", "choicer"] = "basic") -> List[Rerank]:
data = list()
for index, text in enumerate(input):
content = self.PROMPT_LLM_BASED.format(prompt=prompt, text=text)
if type == "basic":
for index, text in enumerate(input):
content = self.PROMPT_LLM_BASED.format(prompt=prompt, text=text)

response = self.model.chat.completions.create(
messages=[{"role": "user", "content": content}], model=self.model.id, temperature=0.1, max_tokens=3, stream=False, n=1
)
result = response.choices[0].message.content
match = re.search(r"[0-1]", result)
result = int(match.group(0)) if match else 0
data.append(Rerank(score=result, index=index))

elif type == "choicer":
text = "\n-------\n".join(input)
prompt = self.PROMPT_CHOICER.format(prompt=prompt, text=text)
response = self.model.chat.completions.create(
messages=[{"role": "user", "content": content}], model=self.model.id, temperature=0.1, max_tokens=3, stream=False, n=1
messages=[{"role": "user", "content": prompt}], model=self.model.id, temperature=0.1, max_tokens=3, stream=False
)
result = response.choices[0].message.content
match = re.search(r"[0-1]", result)
result = int(match.group(0)) if match else 0
data.append(Rerank(score=result, index=index))
match = re.search(r"[0-4]", result)
score = int(match.group(0)) if match else 0
data = [Rerank(score=score, index=0)]

return data
Loading