From 7a40c76d5c6bd46bcdafb55b29b086ba2bb5f8da Mon Sep 17 00:00:00 2001 From: yashbonde Date: Sat, 14 Oct 2023 03:28:50 +0530 Subject: [PATCH 01/14] [chore] improve UI --- chainfury/agent.py | 3 + client/src/components/ChatComp.tsx | 29 +-- client/src/components/NewBotModel.tsx | 134 ++++------- client/src/components/Sidebar.tsx | 59 +++-- client/src/pages/Dashboard/index.tsx | 48 +--- client/src/pages/FlowViewer/index.tsx | 40 ++-- client/src/redux/services/auth.ts | 28 ++- server/chainfury_server/api/chains.py | 21 +- server/chainfury_server/api/fury.py | 250 -------------------- server/chainfury_server/api/prompts.py | 14 +- server/chainfury_server/app.py | 9 - server/chainfury_server/database.py | 88 ++----- server/chainfury_server/engines/fury.py | 38 +-- server/chainfury_server/engines/registry.py | 13 - server/chainfury_server/utils.py | 2 + 15 files changed, 197 insertions(+), 579 deletions(-) delete mode 100644 server/chainfury_server/api/fury.py diff --git a/chainfury/agent.py b/chainfury/agent.py index ddbdf8b2..013efcd8 100644 --- a/chainfury/agent.py +++ b/chainfury/agent.py @@ -170,6 +170,9 @@ def register( Returns: Node: Node """ + if len(node_id) > 80: + raise ValueError(f"Node id '{node_id}' is too long, max length is 80") + logger.debug(f"Registering p-node '{node_id}'") if node_id in self.nodes: raise Exception(f"Node '{node_id}' already registered") diff --git a/client/src/components/ChatComp.tsx b/client/src/components/ChatComp.tsx index cb6de208..95f15c16 100644 --- a/client/src/components/ChatComp.tsx +++ b/client/src/components/ChatComp.tsx @@ -101,20 +101,17 @@ const ChatComp = ({ chatId }: { chatId?: string }) => { return (
{isChatOpen ? ( <> -
+
Hi there! How can I help?
@@ -126,16 +123,14 @@ const ChatComp = ({ chatId }: { chatId?: string }) => { className={`chat ${chatVal.isSender ? 'nbx-chat-end' : 'nbx-chat-start'}`} >
{chatVal.message}
diff --git a/client/src/components/NewBotModel.tsx b/client/src/components/NewBotModel.tsx index c42cda71..448b89ae 100644 --- a/client/src/components/NewBotModel.tsx +++ b/client/src/components/NewBotModel.tsx @@ -6,13 +6,17 @@ import { useAuthStates } from '../redux/hooks/dispatchHooks'; import SvgClose from './SvgComps/Close'; import { ChainFuryContext } from '../App'; +import { + useCreateChainMutation, +} from '../redux/services/auth'; + const NewBotModel = ({ onClose }: { onClose: () => void }) => { const navigate = useNavigate(); - const [botName, setBotName] = useState(''); - const [selectedFlow, setSelectedFlow] = useState('scratch'); - const [selectedTemplate, setSelectedTemplate] = useState(''); + const [chainName, setChainName] = useState(''); + const [chainDescription, setChainDescription] = useState(''); const { auth } = useAuthStates(); const { engine, setEngine } = useContext(ChainFuryContext); + const [createBot] = useCreateChainMutation(); return ( @@ -25,103 +29,47 @@ const NewBotModel = ({ onClose }: { onClose: () => void }) => { /> { - setBotName(e.target.value?.replace(' ', '_')); + setChainName(e.target.value?.replace(' ', '-')); }} - value={botName} + value={chainName} type="text" placeholder="Name" className="h-[40px] w-full mt-[16px]" /> - {selectedFlow === 'scratch' ? ( -
-
{ - setEngine('fury'); - }} - className={`${ - engine === 'fury' ? 'border-light-primary-blue-400 bg-light-primary-blue-50' : '' - } p-[16px] w-[50%] border-light-neutral-grey-200 rounded-md border cursor-pointer`} - > - Fury -
-
{ - setEngine('langchain'); - }} - className={`${ - engine !== 'fury' ? 'border-light-primary-blue-400 bg-light-primary-blue-50' : '' - } p-[16px] w-[50%] border-light-neutral-grey-200 rounded-md border cursor-pointer`} - > - Langchain -
-
- ) : ( - '' - )} -
-
{ - setSelectedFlow('scratch'); - }} - className={`${ - selectedFlow === 'scratch' - ? 'border-light-primary-blue-400 bg-light-primary-blue-50' - : '' - } p-[16px] w-[50%] border-light-neutral-grey-200 rounded-md border cursor-pointer`} - > - Start from scratch -
-
{ - setSelectedFlow('template'); - }} - className={`${ - selectedFlow !== 'scratch' - ? 'border-light-primary-blue-400 bg-light-primary-blue-50' - : '' - } p-[16px] w-[50%] border-light-neutral-grey-200 rounded-md border cursor-pointer`} - > - Create from template -
-
- {selectedFlow === 'template' ? ( -
- {Object.values(auth?.templates)?.map((template, key) => ( -
{ - setSelectedTemplate(template?.id); - if (template?.dag?.main_out) { - setEngine('fury'); - } else { - setEngine('langchain'); - } - }} - className={`${ - selectedTemplate === template?.id - ? 'border-light-primary-blue-400 bg-light-primary-blue-50' - : '' - } cursor-pointer flex flex-col gap-[4px] - p-[8px] border rounded-md border-light-neutral-grey-200 - bg-light-system-bg-primary`} - > - {template?.name} - {template?.description} -
- ))} -
- ) : ( - '' - )} + { + setChainDescription(e.target.value ?? ''); + }} + value={chainDescription} + type="text" + placeholder="Description" + className="h-[100px] w-full mt-[16px]" + /> +
) : ( - + // + // '' )} {newAction ? setNewAction(false)} /> : ''} @@ -162,7 +151,8 @@ const Sidebar = () => { {!flow_id ? ( <>
- Bots +
+ 🦋 Chains {Object.values(auth?.chatBots ?? [])?.map((bot, key) => { return (
{ )}
+ + Swagger + + + + { ); const [searchParams] = useSearchParams(); const [chainMetrics, setMetrics] = useState( - [] as { - name: string; - value: number; - }[] + {} as MetricsInterface ) useEffect(() => { @@ -85,14 +82,9 @@ const Dashboard = () => { }) ?.unwrap() ?.then((res) => { - // console.log(res); - setMetrics([ - { - name: "total_conversations", - value: res?.metrics?.total_conversations - } - ]); - // console.log(chainMetrics); + setMetrics(res?.metrics); + setLatencies(res?.latencies); + console.log(res); }); }; @@ -105,7 +97,7 @@ const Dashboard = () => { <>
{auth?.selectedChatBot?.name ?? 'Nimblebox Bot'} - + */}
- {/* {chainMetrics ? ( + {chainMetrics ? ( ) : ( '' - )} */} - Metrics will show up here + )}
- Try out the bot by clicking on The Bot in the - bottom right corner of your screen. Or embed the bot on your website by adding the - following code to your HTML + Clicking on bottom chat icon to run {auth?.selectedChatBot?.name} + Or embed the bot on your website by adding the following code to your HTML
{ label="Prompts" values={auth?.prompts?.[auth?.selectedChatBot?.id]?.map((prompt) => [ prompt?.id, - new Date(prompt?.created_at).toLocaleString('en-US', { - timeZone: 'utc', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - day: 'numeric', - month: 'long', - year: 'numeric' - }), + new Date(prompt?.created_at).toLocaleString() + " UTC", + Math.round(prompt?.time_taken) + 's', prompt?.input_prompt, prompt?.response, - prompt?.user_rating ?? '', - prompt?.num_tokens ?? '', - Math.round(prompt?.time_taken) + 's', ])} headings={[ 'ID', 'Timestamp', + 'Response Time', 'Input Prompt', 'Final Prompt', - 'User Rating', - '# of Tokens', - 'Response Time', ]} /> ) : ( diff --git a/client/src/pages/FlowViewer/index.tsx b/client/src/pages/FlowViewer/index.tsx index 8a28dcea..be89859e 100644 --- a/client/src/pages/FlowViewer/index.tsx +++ b/client/src/pages/FlowViewer/index.tsx @@ -19,7 +19,7 @@ import { useAuthStates } from '../../redux/hooks/dispatchHooks'; import { useAppDispatch } from '../../redux/hooks/store'; import { useComponentsMutation, - useCreateBotMutation, + useCreateChainMutation, useEditBotMutation, useFuryComponentDetailsMutation, useFuryComponentsMutation @@ -48,7 +48,7 @@ const FlowViewer = () => { const location = useLocation(); const dispatch = useAppDispatch(); const [templateId, setTemplateId] = useState('' as string); - const [createBot] = useCreateBotMutation(); + const [createBot] = useCreateChainMutation(); const [editBot] = useEditBotMutation(); const [furyCompDetails] = useFuryComponentDetailsMutation(); const { auth } = useAuthStates(); @@ -198,11 +198,11 @@ const FlowViewer = () => { engine === 'langchain' ? { name: botName, nodes, edges, token: auth?.accessToken, engine: 'langflow' } : { - name: botName, - engine: engine, - token: auth?.accessToken, - ...TranslateNodes({ nodes, edges }) - } + name: botName, + engine: engine, + token: auth?.accessToken, + ...TranslateNodes({ nodes, edges }) + } ) .unwrap() ?.then((res) => { @@ -315,20 +315,20 @@ const FlowViewer = () => { editBot( engine === 'langchain' ? { - id: flow_id, - name: auth?.chatBots?.[flow_id]?.name, - nodes, - edges, - token: auth?.accessToken, - engine: 'langflow' - } + id: flow_id, + name: auth?.chatBots?.[flow_id]?.name, + nodes, + edges, + token: auth?.accessToken, + engine: 'langflow' + } : { - id: flow_id, - name: auth?.chatBots?.[flow_id]?.name, - engine: engine, - token: auth?.accessToken, - ...TranslateNodes({ nodes, edges }) - } + id: flow_id, + name: auth?.chatBots?.[flow_id]?.name, + engine: engine, + token: auth?.accessToken, + ...TranslateNodes({ nodes, edges }) + } ) .unwrap() ?.then((res) => { diff --git a/client/src/redux/services/auth.ts b/client/src/redux/services/auth.ts index 76397727..c0e04fdd 100644 --- a/client/src/redux/services/auth.ts +++ b/client/src/redux/services/auth.ts @@ -194,7 +194,7 @@ export const authApi = createApi({ } >({ query: (credentials) => ({ - url: `${BASE_URL}/api/prompts/?chatbot_id=${credentials?.id}&limit=50&offset=0`, + url: `${BASE_URL}/api/prompts/?chain_id=${credentials?.id}&limit=50&offset=0`, method: 'GET' }) }), @@ -242,7 +242,7 @@ export const authApi = createApi({ }), // create chain API - createBot: builder.mutation< + createChain: builder.mutation< DEFAULT_RESPONSE, { name: string; @@ -250,23 +250,25 @@ export const authApi = createApi({ edges: any; token: string; engine: string; + description?: string; sample?: Record; main_in?: string; main_out?: string; } >({ - query: (credentials) => ({ + query: (request) => ({ url: `${BASE_URL}/api/chains/`, - method: 'POST', + method: 'PUT', body: { - engine: credentials.engine, - name: credentials.name, + engine: request.engine, + name: request.name, + description: request.description ?? '', dag: { - nodes: credentials.nodes ?? [], - edges: credentials.edges ?? [], - sample: credentials.sample ?? undefined, - main_in: credentials.main_in ?? undefined, - main_out: credentials.main_out ?? undefined + nodes: request.nodes ?? [], + edges: request.edges ?? [], + sample: request.sample ?? undefined, + main_in: request.main_in ?? undefined, + main_out: request.main_out ?? undefined } } }) @@ -353,16 +355,16 @@ export const { // useGetAllBotMetricsMutation, useFuryComponentsMutation, useFuryComponentDetailsMutation, - useNewActionMutation, useGetActionsMutation, + useNewActionMutation, useGetPromptsMutation, useGetStepsMutation, useDeletePromptMutation, useGetBotsMutation, - useCreateBotMutation, + useCreateChainMutation, useEditBotMutation, useProcessPromptMutation, } = authApi; diff --git a/server/chainfury_server/api/chains.py b/server/chainfury_server/api/chains.py index a5a8689c..f0dceff0 100644 --- a/server/chainfury_server/api/chains.py +++ b/server/chainfury_server/api/chains.py @@ -1,6 +1,6 @@ import json import time -from datetime import datetime +from datetime import datetime, timedelta from typing import Annotated, List, Union from sqlalchemy.orm import Session from sqlalchemy.sql import func @@ -261,4 +261,21 @@ def get_chain_metrics( # DB call results = db.query(func.count()).filter(DB.Prompt.chatbot_id == id).all() # type: ignore - return {"total_conversations": results[0][0]} + metrics = {"total_conversations": results[0][0]} + + hourly_average_latency = ( + db.query(DB.Prompt) + .filter(DB.Prompt.chatbot_id == id) # type: ignore + .filter(DB.Prompt.created_at >= datetime.now() - timedelta(hours=24)) + .with_entities( + (func.substr(DB.Prompt.created_at, 1, 14)).label("hour"), + func.avg(DB.Prompt.time_taken).label("avg_time_taken"), + ) + .group_by((func.substr(DB.Prompt.created_at, 1, 14))) + .all() + ) + latency_per_hour = [] + for item in hourly_average_latency: + created_datetime = item[0] + "00:00" + latency_per_hour.append({"created_at": created_datetime, "time": item[1]}) + return {"metrics": metrics, "latencies": latency_per_hour} diff --git a/server/chainfury_server/api/fury.py b/server/chainfury_server/api/fury.py deleted file mode 100644 index d0c91607..00000000 --- a/server/chainfury_server/api/fury.py +++ /dev/null @@ -1,250 +0,0 @@ -import traceback -from uuid import uuid4 -from functools import lru_cache -from sqlalchemy.orm import Session -from typing import Annotated, Union -from sqlalchemy.exc import IntegrityError -from fastapi import APIRouter, Depends, Header, Request, Response, Query, HTTPException - -from chainfury.agent import model_registry, programatic_actions_registry, ai_actions_registry, memory_registry -from chainfury.base import Node - -import chainfury_server.database as DB -import chainfury.types as T -from chainfury_server.utils import logger - -# build router -fury_router = APIRouter(tags=["fury"]) - - -@lru_cache(1) -def _components(to_dict: bool = False): - _MODEL = "models" - _PROGRAMATIC = "programatic_actions" - _BUILTIN_AI = "builtin_ai" - _MEMORY = "memory" - - return { - _MODEL: model_registry.get_models(), - _PROGRAMATIC: programatic_actions_registry.get_nodes(), - _BUILTIN_AI: ai_actions_registry.get_nodes(), - _MEMORY: memory_registry.get_nodes(), - } - - -# -# Components API: this is used for models, actions_p and builtin_ai -# - - -def get_component( - req: Request, - resp: Response, - token: Annotated[str, Header()], - component_type: str, - component_id: str, - db: Session = Depends(DB.fastapi_db_session), -): - user = DB.get_user_from_jwt(token=token, db=db) - - if component_type not in _components(): - resp.status_code = 404 - return T.ApiResponse(message="Component type not found") - - comps = _components()[component_type] - item = comps.get(component_id, None) - if not item: - resp.status_code = 404 - return {} - return item - - -# -# Fury Actions specific APIs -# - - -# C - Create a new FuryAction -def create_action( - req: Request, - resp: Response, - token: Annotated[str, Header()], - fury_action: T.ApiAction, - db: Session = Depends(DB.fastapi_db_session), -) -> Union[T.ApiAction, T.ApiResponse]: - # validate user - user = DB.get_user_from_jwt(token=token, db=db) - - # validate action - node = validate_action(fury_action, resp) - - # insert into db - try: - _node = node.to_dict() - _node["created_by"] = user.id - _node["name"] = fury_action.name - fury_action_data = DB.FuryActions(**_node) - db.add(fury_action_data) - db.commit() - db.refresh(fury_action_data) - return fury_action_data # TODO: @yashbonde - this is a bug, it should return the fury_action - except IntegrityError: - resp.status_code = 409 - return T.ApiResponse(message="FuryAction already exists") - except Exception as e: - logger.exception(traceback.format_exc()) - resp.status_code = 500 - return T.ApiResponse(message="Internal server error") - - -# R - Retrieve a FuryAction by ID -def get_action( - req: Request, - resp: Response, - token: Annotated[str, Header()], - fury_id: str, - db: Session = Depends(DB.fastapi_db_session), -) -> Union[T.ApiAction, T.ApiResponse]: - # validate user - user = DB.get_user_from_jwt(token=token, db=db) - - # read from db - fury_action = db.query(DB.FuryActions).get(fury_id) - if not fury_action: - resp.status_code = 404 - return T.ApiResponse(message="FuryAction not found") - return fury_action # TODO: @yashbonde - this is a bug, it should return the fury_action - - -# U - Update an existing FuryAction -def update_action( - req: Request, - resp: Response, - token: Annotated[str, Header()], - fury_id: str, - fury_action: T.ApiActionUpdateRequest, - db: Session = Depends(DB.fastapi_db_session), -) -> Union[T.ApiAction, T.ApiResponse]: - # validate user - user = DB.get_user_from_jwt(token=token, db=db) - - # validate fields - if not len(fury_action.update_fields): - resp.status_code = 400 - return T.ApiResponse(message="No update fields provided") - - unq_fields = set(fury_action.update_fields) - valid_fields = {"name", "description", "tags", "fn"} - if not unq_fields.issubset(valid_fields): - resp.status_code = 400 - return T.ApiResponse(message=f"Invalid update fields provided {unq_fields - valid_fields}") - for field in unq_fields: - if not getattr(fury_action, field): - resp.status_code = 400 - return T.ApiResponse(message=f"Field {field} cannot be empty") - - # create update dict - update_dict = {} - node = None - for field in unq_fields: - if field == "name": - update_dict["name"] = fury_action.name - - elif field == "description": - update_dict["description"] = fury_action.description - - elif field == "tags": - update_dict["tags"] = fury_action.tags - - elif field == "fn": - node = validate_action(fury_action, resp) - update_dict.update(node.to_dict()) - - # find object - fury_action_db: DB.FuryActions = db.query(DB.FuryActions).get(fury_id) - if not fury_action_db: - resp.status_code = 404 - return T.ApiResponse(message="FuryAction not found") - - # update object - try: - fury_action_db.update_from_dict(update_dict) - db.commit() - db.refresh(fury_action_db) - except Exception as e: - logger.exception(traceback.format_exc()) - resp.status_code = 500 - return T.ApiResponse(message="Internal server error") - return fury_action_db # TODO: @yashbonde - this is a bug, it should return the fury_action - - -# D - Delete a FuryAction by ID -def delete_action( - req: Request, - resp: Response, - token: Annotated[str, Header()], - fury_id: str, - db: Session = Depends(DB.fastapi_db_session), -) -> T.ApiResponse: - # validate user - user = DB.get_user_from_jwt(token=token, db=db) - - # delete from db - fury_action = db.query(DB.FuryActions).get(fury_id) - if not fury_action: - resp.status_code = 404 - return T.ApiResponse(message="FuryAction not found") - db.delete(fury_action) - db.commit() - return T.ApiResponse(message="FuryAction deleted successfully") - - -# L - List all DB.FuryActions -def list_actions( - req: Request, - resp: Response, - token: Annotated[str, Header()], - offset: int = Query(0, ge=0), - limit: int = Query(25, ge=1, le=25), - db: Session = Depends(DB.fastapi_db_session), -): - # validate user - user = DB.get_user_from_jwt(token=token, db=db) - - # read from db - fury_actions = db.query(DB.FuryActions).offset(offset).limit(limit).all() # type: ignore - return fury_actions - - -# -# helper functions -# - - -def validate_action(fury_action: Union[T.ApiAction, T.ApiActionUpdateRequest], resp: Response) -> Node: - # if the function is to be updated then perform the full validation same as when creating a new action - if len(fury_action.outputs) != 1: - raise HTTPException(status_code=400, detail=f"Only one output must be provided when modifying the function") - - try: - fury_action.outputs = fury_action.outputs[0].dict() # type: ignore - fury_action.outputs = {fury_action.outputs["name"]: fury_action.outputs["loc"]} # type: ignore - except Exception as e: - logger.exception(traceback.format_exc()) - raise HTTPException(status_code=400, detail=f"Cannot parse function: {e}") - - try: - node: Node = ai_actions_registry.to_action( - action_name=fury_action.name, - node_id=str(uuid4()), - model_id=fury_action.fn.model_id, - model_params=fury_action.fn.model_params, - fn=fury_action.fn.fn, - outputs=fury_action.outputs, - description=fury_action.description, - ) - except Exception as e: - logger.exception(traceback.format_exc()) - raise HTTPException(status_code=400, detail=f"Cannot parse function: {e}") - - return node # type: ignore diff --git a/server/chainfury_server/api/prompts.py b/server/chainfury_server/api/prompts.py index 8d110c63..3cf928d2 100644 --- a/server/chainfury_server/api/prompts.py +++ b/server/chainfury_server/api/prompts.py @@ -11,7 +11,7 @@ def list_prompts( token: Annotated[str, Header()], - chatbot_id: str, + chain_id: str, limit: int = 100, offset: int = 0, db: Session = Depends(DB.fastapi_db_session), @@ -25,7 +25,7 @@ def list_prompts( offset = offset if offset > 0 else 0 prompts = ( db.query(DB.Prompt) # type: ignore - .filter(DB.Prompt.chatbot_id == chatbot_id) + .filter(DB.Prompt.chatbot_id == chain_id) .order_by(DB.Prompt.created_at.desc()) .limit(limit) .offset(offset) @@ -47,10 +47,7 @@ def get_prompt( if not prompt: raise HTTPException(status_code=404, detail="Prompt not found") - irsteps = db.query(DB.IntermediateStep).filter(DB.IntermediateStep.prompt_id == prompt.session_id).all() # type: ignore - if not irsteps: - irsteps = [] - return {"prompt": prompt.to_dict(), "irsteps": [ir.to_dict() for ir in irsteps]} + return {"prompt": prompt.to_dict()} def delete_prompt( @@ -67,11 +64,6 @@ def delete_prompt( raise HTTPException(status_code=404, detail="Prompt not found") db.delete(prompt) - # now delete all the intermediate steps - ir_steps = db.query(DB.IntermediateStep).filter(DB.IntermediateStep.prompt_id == prompt.id).all() # type: ignore - for ir in ir_steps: - db.delete(ir) - db.commit() return {"msg": f"Prompt: '{prompt_id}' deleted"} diff --git a/server/chainfury_server/app.py b/server/chainfury_server/app.py index 37649071..14f3db72 100644 --- a/server/chainfury_server/app.py +++ b/server/chainfury_server/app.py @@ -11,7 +11,6 @@ # API function imports import chainfury_server.api.user as api_user import chainfury_server.api.chains as api_chains -import chainfury_server.api.fury as api_fury import chainfury_server.api.prompts as api_prompts app = FastAPI( @@ -49,14 +48,6 @@ app.add_api_route("/api/chains/{id}/", api_chains.run_chain, methods=["POST"], tags=["chains"], response_model=None) # type: ignore app.add_api_route("/api/chains/{id}/metrics/", api_chains.get_chain_metrics, methods=["GET"], tags=["chains"]) # type: ignore -# actions -app.add_api_route("/api/fury/", api_fury.list_actions, methods=["GET"], tags=["fury"]) # type: ignore -app.add_api_route("/api/fury/", api_fury.create_action, methods=["PUT"], tags=["fury"]) # type: ignore -app.add_api_route("/api/fury/{fury_id}/", api_fury.get_action, methods=["GET"], tags=["fury"]) # type: ignore -app.add_api_route("/api/fury/{fury_id}/", api_fury.delete_action, methods=["DELETE"], tags=["fury"]) # type: ignore -app.add_api_route("/api/fury/{fury_id}/", api_fury.update_action, methods=["PATCH"], tags=["fury"]) # type: ignore -# app.add_api_route("/api/fury/{fury_id}/", fury.run_action, methods=["POST"], tags=["fury"]) # type: ignore - # prompts app.add_api_route("/api/prompts/", api_prompts.list_prompts, methods=["GET"], tags=["prompts"]) # type: ignore app.add_api_route("/api/prompts/{prompt_id}/", api_prompts.get_prompt, methods=["GET"], tags=["prompts"]) # type: ignore diff --git a/server/chainfury_server/database.py b/server/chainfury_server/database.py index 83d5866c..76782f72 100644 --- a/server/chainfury_server/database.py +++ b/server/chainfury_server/database.py @@ -94,26 +94,26 @@ def fastapi_db_session(): db.close() -def unique_string(table, row_reference): +def unique_string(table, row_reference, length=ID_LENGTH): """ Gets Random Unique String for Primary key and makes sure its unique for the table. """ db = db_session() - random_string = get_random_alphanumeric_string(ID_LENGTH).lower() + random_string = get_random_alphanumeric_string(length).lower() while db.query(table).filter(row_reference == random_string).limit(1).first() is not None: # type: ignore - random_string = get_random_alphanumeric_string(ID_LENGTH).lower() + random_string = get_random_alphanumeric_string(length).lower() return random_string -def unique_number(Table, row_reference): +def unique_number(Table, row_reference, length=ID_LENGTH): """ Gets Random Unique Number for Primary key and makes sure its unique for the table. """ db = db_session() - random_number = get_random_number(ID_LENGTH) + random_number = get_random_number(length) while db.query(Table).filter(row_reference == random_number).limit(1).first() is not None: # type: ignore - random_number = get_random_number(ID_LENGTH) + random_number = get_random_number(length) return random_number @@ -270,34 +270,20 @@ def to_dict(self): } -class IntermediateStep(Base): - __tablename__ = "intermediate_step" +class ChainLog(Base): + __tablename__ = "chain_logs" id = Column( - String(8), - default=lambda: unique_string(IntermediateStep, IntermediateStep.id), + String(16), + default=lambda: unique_string(ChainLog, ChainLog.id, 16), primary_key=True, ) - prompt_id = Column(Integer, ForeignKey("prompt.id"), nullable=False) - intermediate_prompt = Column(Text, nullable=False) - intermediate_response = Column(Text, nullable=False) - response_json = Column(JSON, nullable=True) - meta = Column(JSON) created_at = Column(DateTime, nullable=False) - - def to_dict(self): - return { - "id": self.id, - "prompt_id": self.prompt_id, - "intermediate_prompt": self.intermediate_prompt, - "intermediate_response": self.intermediate_response, - "response_json": self.response_json, - "meta": self.meta, - "created_at": self.created_at, - } - - def __repr__(self): - return f"IntermediateStep(id={self.id}, prompt_id={self.prompt_id}, intermediate_prompt={self.intermediate_prompt}, intermediate_response={self.intermediate_response}, response_json={self.response_json}, meta={self.meta}, created_at={self.created_at})" + prompt_id = Column(Integer, ForeignKey("prompt.id"), nullable=False) + node_id = Column(String(Env.CFS_MAX_NODE_ID_LEN()), nullable=False) + worker_id = Column(String(Env.CF_MAX_WORKER_ID_LEN()), nullable=False) + message = Column(Text, nullable=False) + data = Column(JSON, nullable=True) class Template(Base): @@ -322,50 +308,6 @@ def to_dict(self): "meta": self.meta, } - def __repr__(self): - return f"Template(id={self.id}, name={self.name}, description={self.description}, dag={self.dag}, meta={self.meta})" - - -# A fury action is an AI powered node that can be used in a fury chain it is the DB equivalent of fury.Node - - -class FuryActions(Base): - __tablename__ = "fury_actions" - - id: Column = Column( - String(36), - primary_key=True, - ) - created_by: str = Column(String(8), ForeignKey("user.id"), nullable=False) - type: str = Column(String(80), nullable=False) # the AI Action type - name: str = Column(String(80), unique=False) - description: str = Column(String(80)) - fields: list[dict] = Column(JSON) - fn: dict = Column(JSON) - outputs: list[dict] = Column(JSON) - tags: list[str] = Column(JSON) - - def to_dict(self): - return { - "id": self.id, - "created_by": self.created_by, - "type": self.type, - "name": self.name, - "description": self.description, - "fields": self.fields, - "fn": self.fn, - "outputs": self.outputs, - "tags": self.tags, - } - - def update_from_dict(self, data: dict): - self.name = data.get("name", self.name) - self.description = data.get("description", self.description) - self.fields = data.get("fields", self.fields) - self.fn = data.get("fn", self.fn) - self.outputs = data.get("outputs", self.outputs) - self.tags = data.get("tags", self.tags) - Base.metadata.create_all(bind=engine) # type: ignore diff --git a/server/chainfury_server/engines/fury.py b/server/chainfury_server/engines/fury.py index 25eee4dc..bc4357a1 100644 --- a/server/chainfury_server/engines/fury.py +++ b/server/chainfury_server/engines/fury.py @@ -157,28 +157,28 @@ def __call__(self, thought): intermediate_response = "" if type(intermediate_response) != str: intermediate_response = str(intermediate_response) - create_intermediate_steps(self.db, prompt_id=self.prompt_id, intermediate_response=intermediate_response) + # create_intermediate_steps(self.db, prompt_id=self.prompt_id, intermediate_response=intermediate_response) self.count += 1 -def create_intermediate_steps( - db: Session, - prompt_id: int, - intermediate_prompt: str = "", - intermediate_response: str = "", - response_json: Dict = {}, -) -> DB.IntermediateStep: - db_prompt = DB.IntermediateStep( - prompt_id=prompt_id, - intermediate_prompt=intermediate_prompt, - intermediate_response=intermediate_response, - response_json=response_json, - created_at=SimplerTimes.get_now_datetime(), - ) # type: ignore - db.add(db_prompt) - db.commit() - db.refresh(db_prompt) - return db_prompt +# def create_intermediate_steps( +# db: Session, +# prompt_id: int, +# intermediate_prompt: str = "", +# intermediate_response: str = "", +# response_json: Dict = {}, +# ) -> DB.IntermediateStep: +# db_prompt = DB.IntermediateStep( +# prompt_id=prompt_id, +# intermediate_prompt=intermediate_prompt, +# intermediate_response=intermediate_response, +# response_json=response_json, +# created_at=SimplerTimes.get_now_datetime(), +# ) # type: ignore +# db.add(db_prompt) +# db.commit() +# db.refresh(db_prompt) +# return db_prompt def create_prompt(db: Session, chatbot_id: str, input_prompt: str, session_id: str) -> DB.Prompt: diff --git a/server/chainfury_server/engines/registry.py b/server/chainfury_server/engines/registry.py index ed5b063b..941ba162 100644 --- a/server/chainfury_server/engines/registry.py +++ b/server/chainfury_server/engines/registry.py @@ -10,19 +10,6 @@ class EngineInterface(object): def engine_name(self) -> str: raise NotImplementedError("Subclass this and implement engine_name") - def __call__( - self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: float, stream: bool = False, as_task: bool = False - ): - """ - This is the main entry point for the engine. It should return a CFPromptResult. - """ - if as_task: - return self.submit(chatbot=chatbot, prompt=prompt, db=db, start=start) - elif stream: - return self.stream(chatbot=chatbot, prompt=prompt, db=db, start=start) - else: - return self.run(chatbot=chatbot, prompt=prompt, db=db, start=start) - def run(self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: float) -> T.CFPromptResult: """ This is the main entry point for the engine. It should return a CFPromptResult. diff --git a/server/chainfury_server/utils.py b/server/chainfury_server/utils.py index fe85b090..83fe48c1 100644 --- a/server/chainfury_server/utils.py +++ b/server/chainfury_server/utils.py @@ -18,6 +18,8 @@ class Env: # when you want to use chainfury as a client you need to set the following vars CFS_DATABASE = lambda x: os.getenv("CFS_DATABASE", x) JWT_SECRET = lambda: os.getenv("JWT_SECRET", "hajime-shimamoto") + CFS_MAX_NODE_ID_LEN = lambda: int(os.getenv("CFS_MAX_NODE_ID_LEN", 80)) + CF_MAX_WORKER_ID_LEN = lambda: int(os.getenv("CF_MAX_WORKER_ID_LEN", 16)) def folder(x: str) -> str: From 21639158bb643dd57e9fa4262654f3cfa71684c8 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Mon, 16 Oct 2023 14:24:27 +0530 Subject: [PATCH 02/14] custom landing page --- .gitignore | 3 +- server/chainfury_server/api/chains.py | 29 +++++++++- server/chainfury_server/app.py | 11 +--- server/chainfury_server/engines/fury.py | 28 ++++++++- server/chainfury_server/engines/registry.py | 28 ++++++++- server/chainfury_server/landing.py | 63 +++++++++++++++++++++ 6 files changed, 144 insertions(+), 18 deletions(-) create mode 100644 server/chainfury_server/landing.py diff --git a/.gitignore b/.gitignore index e75562fe..8d215a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -150,4 +150,5 @@ api_docs/_build/ api_docs/_static/ cf locust_file.py -demo/ \ No newline at end of file +demo/ +logs.py diff --git a/server/chainfury_server/api/chains.py b/server/chainfury_server/api/chains.py index f0dceff0..3afec391 100644 --- a/server/chainfury_server/api/chains.py +++ b/server/chainfury_server/api/chains.py @@ -198,6 +198,8 @@ def run_chain( prompt: T.ApiPromptBody, stream: bool = False, as_task: bool = False, + store_ir: bool = False, + store_io: bool = False, db: Session = Depends(DB.fastapi_db_session), ) -> Union[StreamingResponse, T.CFPromptResult, T.ApiResponse]: """ @@ -227,7 +229,14 @@ def run_chain( # if as_task: - result = engine.submit(chatbot=chatbot, prompt=prompt, db=db, start=time.time()) + result = engine.submit( + chatbot=chatbot, + prompt=prompt, + db=db, + start=time.time(), + store_ir=store_ir, + store_io=store_io, + ) return result elif stream: @@ -242,10 +251,24 @@ def _get_streaming_response(result): result = {**ir, "done": done} yield json.dumps(result) + "\n" - streaming_result = engine.stream(chatbot=chatbot, prompt=prompt, db=db, start=time.time()) + streaming_result = engine.stream( + chatbot=chatbot, + prompt=prompt, + db=db, + start=time.time(), + store_ir=store_ir, + store_io=store_io, + ) return StreamingResponse(content=_get_streaming_response(streaming_result)) else: - result = engine.run(chatbot=chatbot, prompt=prompt, db=db, start=time.time()) + result = engine.run( + chatbot=chatbot, + prompt=prompt, + db=db, + start=time.time(), + store_ir=store_ir, + store_io=store_io, + ) return result diff --git a/server/chainfury_server/app.py b/server/chainfury_server/app.py index 14f3db72..a551d27a 100644 --- a/server/chainfury_server/app.py +++ b/server/chainfury_server/app.py @@ -12,6 +12,7 @@ import chainfury_server.api.user as api_user import chainfury_server.api.chains as api_chains import chainfury_server.api.prompts as api_prompts +from chainfury_server.landing import landing_page app = FastAPI( title="ChainFury", @@ -34,6 +35,8 @@ # TODO: deprecate this app.add_api_route("/api/v1/chatbot/{id}/prompt", api_chains.run_chain, methods=["POST"], tags=["deprecated"], response_model=None) # type: ignore +app.add_api_route("/", landing_page, methods=["GET"], tags=["deprecated"], response_class=HTMLResponse) # type: ignore + # user app.add_api_route("/user/login/", api_user.login, methods=["POST"], tags=["user"]) # type: ignore app.add_api_route("/user/signup/", api_user.sign_up, methods=["POST"], tags=["user"]) # type: ignore @@ -58,14 +61,6 @@ # Static files # ------------ - -@app.get("/") -async def serve_framer(): - """Serves the landing page for ChainFury""" - r = requests.get("https://chainfury.framer.website/") - return HTMLResponse(content=r.text, status_code=r.status_code) - - # add static files _static_fp = joinp(folder(__file__), "static") static = Jinja2Templates(directory=_static_fp) diff --git a/server/chainfury_server/engines/fury.py b/server/chainfury_server/engines/fury.py index bc4357a1..f7b56cdd 100644 --- a/server/chainfury_server/engines/fury.py +++ b/server/chainfury_server/engines/fury.py @@ -20,7 +20,15 @@ class FuryEngine(EngineInterface): def engine_name(self) -> str: return "fury" - def run(self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: float) -> T.CFPromptResult: + def run( + self, + chatbot: DB.ChatBot, + prompt: T.ApiPromptBody, + db: Session, + start: float, + store_ir: bool, + store_io: bool, + ) -> T.CFPromptResult: if prompt.new_message and prompt.data: raise HTTPException(status_code=400, detail="prompt cannot have both new_message and data") try: @@ -59,7 +67,13 @@ def run(self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: raise HTTPException(status_code=500, detail=str(e)) from e def stream( - self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: float + self, + chatbot: DB.ChatBot, + prompt: T.ApiPromptBody, + db: Session, + start: float, + store_ir: bool, + store_io: bool, ) -> Generator[Tuple[Union[T.CFPromptResult, Dict[str, Any]], bool], None, None]: if prompt.new_message and prompt.data: raise HTTPException(status_code=400, detail="prompt cannot have both new_message and data") @@ -108,7 +122,15 @@ def stream( logger.exception(e) raise HTTPException(status_code=500, detail=str(e)) from e - def submit(self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: float) -> T.CFPromptResult: + def submit( + self, + chatbot: DB.ChatBot, + prompt: T.ApiPromptBody, + db: Session, + start: float, + store_ir: bool, + store_io: bool, + ) -> T.CFPromptResult: if prompt.new_message and prompt.data: raise HTTPException(status_code=400, detail="prompt cannot have both new_message and data") try: diff --git a/server/chainfury_server/engines/registry.py b/server/chainfury_server/engines/registry.py index 941ba162..597b4d51 100644 --- a/server/chainfury_server/engines/registry.py +++ b/server/chainfury_server/engines/registry.py @@ -10,21 +10,43 @@ class EngineInterface(object): def engine_name(self) -> str: raise NotImplementedError("Subclass this and implement engine_name") - def run(self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: float) -> T.CFPromptResult: + def run( + self, + chatbot: DB.ChatBot, + prompt: T.ApiPromptBody, + db: Session, + start: float, + store_ir: bool, + store_io: bool, + ) -> T.CFPromptResult: """ This is the main entry point for the engine. It should return a CFPromptResult. """ raise NotImplementedError("Subclass this and implement run()") def stream( - self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: float + self, + chatbot: DB.ChatBot, + prompt: T.ApiPromptBody, + db: Session, + start: float, + store_ir: bool, + store_io: bool, ) -> Generator[Tuple[Union[T.CFPromptResult, Dict[str, Any]], bool], None, None]: """ This is the main entry point for the engine. It should return a CFPromptResult. """ raise NotImplementedError("Subclass this and implement stream()") - def submit(self, chatbot: DB.ChatBot, prompt: T.ApiPromptBody, db: Session, start: float) -> T.CFPromptResult: + def submit( + self, + chatbot: DB.ChatBot, + prompt: T.ApiPromptBody, + db: Session, + start: float, + store_ir: bool, + store_io: bool, + ) -> T.CFPromptResult: """ This is the main entry point for the engine. It should return a CFPromptResult. """ diff --git a/server/chainfury_server/landing.py b/server/chainfury_server/landing.py new file mode 100644 index 00000000..fbb68649 --- /dev/null +++ b/server/chainfury_server/landing.py @@ -0,0 +1,63 @@ +# this is the landing page UI for the app + +from time import time +from fastapi.responses import HTMLResponse + + +async def landing_page(): + """Serves the landing page for ChainFury""" + + return HTMLResponse( + status_code=200, + content=""" + + + + + + ChainFury Page + + + + + ChainFury logo + + +

ChainFury is a production grade chaining engine used by TuneChat (formerly ChatNBX)

+ + +
GitHub + Documentation + ChatNBX + + + +""", + ) From 23e5bec56861991e91bc3e48616294777c93e0b1 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Mon, 16 Oct 2023 14:40:49 +0530 Subject: [PATCH 03/14] make storing in DB optional --- server/chainfury_server/engines/fury.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/server/chainfury_server/engines/fury.py b/server/chainfury_server/engines/fury.py index f7b56cdd..a192c5d5 100644 --- a/server/chainfury_server/engines/fury.py +++ b/server/chainfury_server/engines/fury.py @@ -33,7 +33,7 @@ def run( raise HTTPException(status_code=400, detail="prompt cannot have both new_message and data") try: logger.debug("Adding prompt to database") - prompt_row = create_prompt(db, chatbot.id, prompt.new_message, prompt.session_id) # type: ignore + prompt_row = create_prompt(db, chatbot.id, prompt.new_message if store_io else "", prompt.session_id) # type: ignore # Create a Fury chain then run the chain while logging all the intermediate steps dag = T.Dag(**chatbot.dag) # type: ignore @@ -54,7 +54,8 @@ def run( ) # commit the prompt to DB - prompt_row.response = result.result # type: ignore + if store_io: + prompt_row.response = result.result # type: ignore prompt_row.time_taken = float(time.time() - start) # type: ignore db.commit() @@ -79,7 +80,7 @@ def stream( raise HTTPException(status_code=400, detail="prompt cannot have both new_message and data") try: logger.debug("Adding prompt to database") - prompt_row = create_prompt(db, chatbot.id, prompt.new_message, prompt.session_id) # type: ignore + prompt_row = create_prompt(db, chatbot.id, prompt.new_message if store_io else "", prompt.session_id) # type: ignore # Create a Fury chain then run the chain while logging all the intermediate steps dag = T.Dag(**chatbot.dag) # type: ignore @@ -110,7 +111,8 @@ def stream( ) # commit the prompt to DB - prompt_row.response = result.result # type: ignore + if store_io: + prompt_row.response = result.result # type: ignore prompt_row.time_taken = float(time.time() - start) # type: ignore db.commit() @@ -135,7 +137,7 @@ def submit( raise HTTPException(status_code=400, detail="prompt cannot have both new_message and data") try: logger.debug("Adding prompt to database") - prompt_row = create_prompt(db, chatbot.id, prompt.new_message, prompt.session_id) # type: ignore + prompt_row = create_prompt(db, chatbot.id, prompt.new_message if store_io else "", prompt.session_id) # type: ignore # Create a Fury chain then run the chain while logging all the intermediate steps dag = T.Dag(**chatbot.dag) # type: ignore @@ -150,7 +152,8 @@ def submit( prompt_id=prompt_row.id, task_id=task_id, ) - prompt_row.response = result.result # type: ignore + if store_io: + prompt_row.response = result.result # type: ignore prompt_row.time_taken = float(time.time() - start) # type: ignore db.commit() From 18431faa49aefe80a0c41a309cff172b65e39716 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Mon, 16 Oct 2023 16:49:28 +0530 Subject: [PATCH 04/14] [chore] automate generation of components list --- .github/workflows/static.yml | 2 + .../cf_server/chainfury_server.api.fury.rst | 7 -- api_docs/cf_server/chainfury_server.api.rst | 1 - api_docs/examples/components-list.rst | 75 +++++++++++++++++++ api_docs/index.rst | 2 +- api_docs/templates/components-list.rst | 54 +++++++++++++ cf_internal | 2 +- chainfury/agent.py | 2 +- chainfury/base.py | 2 +- chainfury/components/qdrant/__init__.py | 2 + scripts/list_builtins.py | 58 ++++++++++++++ 11 files changed, 195 insertions(+), 12 deletions(-) delete mode 100644 api_docs/cf_server/chainfury_server.api.fury.rst create mode 100644 api_docs/examples/components-list.rst create mode 100644 api_docs/templates/components-list.rst create mode 100644 scripts/list_builtins.py diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index e1387f8a..63041b07 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -46,6 +46,8 @@ jobs: python -m pip install sphinx-rtd-theme==1.2.2 cd server python3 -m pip install . + cd .. + python3 scripts/list_builtins.py ./api_docs/templates/components-list.rst ./api_docs/examples/components-list.rst - name: Build docs run: | diff --git a/api_docs/cf_server/chainfury_server.api.fury.rst b/api_docs/cf_server/chainfury_server.api.fury.rst deleted file mode 100644 index 102e71cc..00000000 --- a/api_docs/cf_server/chainfury_server.api.fury.rst +++ /dev/null @@ -1,7 +0,0 @@ -chainfury\_server.api.fury module -================================= - -.. automodule:: chainfury_server.api.fury - :members: - :undoc-members: - :show-inheritance: diff --git a/api_docs/cf_server/chainfury_server.api.rst b/api_docs/cf_server/chainfury_server.api.rst index a1b7c545..5673b0d8 100644 --- a/api_docs/cf_server/chainfury_server.api.rst +++ b/api_docs/cf_server/chainfury_server.api.rst @@ -13,6 +13,5 @@ Submodules :maxdepth: 4 chainfury_server.api.chains - chainfury_server.api.fury chainfury_server.api.prompts chainfury_server.api.user diff --git a/api_docs/examples/components-list.rst b/api_docs/examples/components-list.rst new file mode 100644 index 00000000..838ce6e3 --- /dev/null +++ b/api_docs/examples/components-list.rst @@ -0,0 +1,75 @@ +Components List +=============== + +.. this is a jinja template document, run scripts/list_builtins.py to generate components-list.rst + +There are several components that are shipped with the ``chainfury``. You can find how to access the underlying function +via the `components page`_. + +.. code-block::python + + # load the registries you can do these imports + from chainfury import programatic_actions_registry, ai_actions_registry + +Programatic Actions +------------------- + +Programatic means that these are generally not an LLM call rather something more standard like calling an API, +transforming the data, etc. + + +* `serper-api` - Search the web with Serper. Copy: ``programatic_actions_registry.get("serper-api")`` + +* `call_api_requests` - Call an API using the requests library. Copy: ``programatic_actions_registry.get("call_api_requests")`` + +* `regex_search` - Perform a regex search on the text and get items in an array. Copy: ``programatic_actions_registry.get("regex_search")`` + +* `regex_substitute` - Perform a regex substitution on the text and get the result. Copy: ``programatic_actions_registry.get("regex_substitute")`` + +* `json_translator` - Extract a value from a JSON object using a list of keys. Copy: ``programatic_actions_registry.get("json_translator")`` + + +AI Action Components +-------------------- + +These actions generally take the input, create a custom prompt, call the Model and respond back with the result. + + +* `hello-world` - Python function loaded from a file used as an AI action. Copy: ``ai_actions_registry.get("hello-world")`` + +* `deep-rap-quote` - J-type action will write a deep poem in the style of a character. Copy: ``ai_actions_registry.get("deep-rap-quote")`` + + +Memory Components +----------------- + +Memory components are used to store data, which can be a Vector DB or Redis, etc. + + +* `qdrant-write` - Write to the Qdrant DB using the Qdrant client. Copy: ``memory_registry.get_write("qdrant-write")`` + +* `qdrant-read` - Function to read from the Qdrant DB using the Qdrant client. Copy: ``memory_registry.get_read("qdrant-read")`` + + +Model Components +---------------- + +Model are the different GenAI models that can be used from the ``chainfury``. + + +* `stability-text-to-image` - Generate a new image from a text prompt. Copy: ``model_registry.get("stability-text-to-image")`` + +* `chatnbx` - Chat with the ChatNBX API with OpenAI compatability, see more at https://chat.nbox.ai/. Copy: ``model_registry.get("chatnbx")`` + +* `nbx-deploy` - Call NimbleBox LLMOps deploy API. Copy: ``model_registry.get("nbx-deploy")`` + +* `openai-completion` - Given a prompt, the model will return one or more predicted completions, and can also return the probabilities of alternative tokens at each position. Copy: ``model_registry.get("openai-completion")`` + +* `openai-chat` - Given a list of messages describing a conversation, the model will return a response. Copy: ``model_registry.get("openai-chat")`` + +* `openai-embedding` - Given a list of messages create embeddings for each message. Copy: ``model_registry.get("openai-embedding")`` + + +.. all the links are here + +.. _components page: https://qdrant.tech/documentation/tutorials/bulk-upload/#upload-directly-to-disk diff --git a/api_docs/index.rst b/api_docs/index.rst index aa9f4566..33a9b758 100644 --- a/api_docs/index.rst +++ b/api_docs/index.rst @@ -48,7 +48,6 @@ Read the latest blog posts: examples/install examples/storing-private-data - examples/use-chainfury-privately .. toctree:: :maxdepth: 2 @@ -65,6 +64,7 @@ Read the latest blog posts: :caption: Integrations source/chainfury.components + examples/components-list .. toctree:: :maxdepth: 2 diff --git a/api_docs/templates/components-list.rst b/api_docs/templates/components-list.rst new file mode 100644 index 00000000..02f58eb9 --- /dev/null +++ b/api_docs/templates/components-list.rst @@ -0,0 +1,54 @@ +Components List +=============== + +.. this is a jinja template document, run scripts/list_builtins.py to generate components-list.rst + +There are several components that are shipped with the ``chainfury``. You can find how to access the underlying function +via the `components page`_. + +.. code-block::python + + # load the registries you can do these imports + from chainfury import programatic_actions_registry, ai_actions_registry + +Programatic Actions +------------------- + +Programatic means that these are generally not an LLM call rather something more standard like calling an API, +transforming the data, etc. + +{% for component in pc %} +* `{{ component.id }}` - {{ component.description }} +{% endfor %} + +AI Action Components +-------------------- + +These actions generally take the input, create a custom prompt, call the Model and respond back with the result. + +{% for component in ac %} +* `{{ component.id }}` - {{ component.description }} +{% endfor %} + +Memory Components +----------------- + +Memory components are used to store data, which can be a Vector DB or Redis, etc. + +{% for component in mc %} +* `{{ component.id }}` - {{ component.description }} +{% endfor %} + +Model Components +---------------- + +Model are the different GenAI models that can be used from the ``chainfury``. + +{% for component in moc %} +* `{{ component.id }}` - {{ component.description }} +{% endfor %} + +.. all the links are here + +.. _components page: https://qdrant.tech/documentation/tutorials/bulk-upload/#upload-directly-to-disk + diff --git a/cf_internal b/cf_internal index be5b3779..5738dec3 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit be5b377978257b95a2cdd7ebff709533825fc20c +Subproject commit 5738dec340709c952372dbff6c3b3505bd40e6d2 diff --git a/chainfury/agent.py b/chainfury/agent.py index 013efcd8..a082e976 100644 --- a/chainfury/agent.py +++ b/chainfury/agent.py @@ -695,7 +695,7 @@ def __call__(self, **data: Dict[str, Any]) -> Any: class MemoryRegistry: def __init__(self) -> None: - self._memories = {} + self._memories: Dict[str, Node] = {} def register_write( self, diff --git a/chainfury/base.py b/chainfury/base.py index c9c11fc3..d8aaf37c 100644 --- a/chainfury/base.py +++ b/chainfury/base.py @@ -577,7 +577,7 @@ def __init__( collection_name: str, id: str, fn: object, - description, + description: str = "", usage: List[Union[str, int]] = [], tags=[], ): diff --git a/chainfury/components/qdrant/__init__.py b/chainfury/components/qdrant/__init__.py index 209394dc..55ac0b96 100644 --- a/chainfury/components/qdrant/__init__.py +++ b/chainfury/components/qdrant/__init__.py @@ -143,6 +143,7 @@ def _insert(): fn=qdrant_write, outputs={"status": 0}, vector_key="embeddings", + description="Write to the Qdrant DB using the Qdrant client", ) @@ -251,6 +252,7 @@ def qdrant_read( fn=qdrant_read, outputs={"items": 0}, vector_key="embeddings", + description="Function to read from the Qdrant DB using the Qdrant client", ) diff --git a/scripts/list_builtins.py b/scripts/list_builtins.py new file mode 100644 index 00000000..e6afa4b3 --- /dev/null +++ b/scripts/list_builtins.py @@ -0,0 +1,58 @@ +from fire import Fire +import jinja2 as j2 +from chainfury import programatic_actions_registry, ai_actions_registry, memory_registry, model_registry + + +def main(src_file: str, trg_file: str, v: bool = False): + with open(src_file, "r") as f: + temp = j2.Template(f.read()) + + # create the components list + pc = [] + for node_id, node in programatic_actions_registry.nodes.items(): + pc.append( + { + "id": node.id, + "description": node.description.rstrip(".") + f'. Copy: ``programatic_actions_registry.get("{node.id}")``', + } + ) + + ac = [] + for node_id, node in ai_actions_registry.nodes.items(): + ac.append( + { + "id": node.id, + "description": node.description.rstrip(".") + f'. Copy: ``ai_actions_registry.get("{node.id}")``', + } + ) + + mc = [] + for node_id, node in memory_registry._memories.items(): + fn = "get_read" if node.id.endswith("-read") else "get_write" + mc.append( + { + "id": node.id, + "description": node.description.rstrip(".") + f'. Copy: ``memory_registry.{fn}("{node.id}")``', + } + ) + + moc = [] + for model_id, model in model_registry.models.items(): + moc.append( + { + "id": model_id, + "description": model.description.rstrip(".") + f'. Copy: ``model_registry.get("{model_id}")``', + } + ) + + op = temp.render(pc=pc, ac=ac, mc=mc, moc=moc) + if v: + print(op) + print("Writing to", trg_file) + + with open(trg_file, "w") as f: + f.write(op) + + +if __name__ == "__main__": + Fire(main) From 70679ae1252f5f5c406c287222fd594ff19df26a Mon Sep 17 00:00:00 2001 From: yashbonde Date: Mon, 16 Oct 2023 17:11:50 +0530 Subject: [PATCH 05/14] [cfi] latest --- cf_internal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf_internal b/cf_internal index 5738dec3..674e56d1 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit 5738dec340709c952372dbff6c3b3505bd40e6d2 +Subproject commit 674e56d1e6bf71be5b2ca717bf1e1fe107d327f6 From 0e175d144f55cbddb9598b5d505f6187015866c6 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Mon, 16 Oct 2023 23:45:15 +0530 Subject: [PATCH 06/14] [cfi] bug fix + update client to use utils.CFEnv --- cf_internal | 2 +- chainfury/client.py | 12 +++++++----- chainfury/utils.py | 4 ++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/cf_internal b/cf_internal index 674e56d1..9d04a229 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit 674e56d1e6bf71be5b2ca717bf1e1fe107d327f6 +Subproject commit 9d04a22930d6a4c3ea33e625f64d5aed76d174a5 diff --git a/chainfury/client.py b/chainfury/client.py index d7a0abe2..a637c4f0 100644 --- a/chainfury/client.py +++ b/chainfury/client.py @@ -3,7 +3,7 @@ from functools import lru_cache from typing import Dict, Any, Tuple -from chainfury.utils import logger +from chainfury.utils import logger, CFEnv class Subway: @@ -11,6 +11,8 @@ class Subway: Simple code that allows writing APIs by `.attr.ing` them. This is inspired from gRPC style functional calls which hides the complexity of underlying networking. This is useful when you are trying to debug live server directly. + **If you want to setup a client, use the ``get_client`` function, this is not what you are looking for.** + Note: User is solely responsible for checking if the certain API endpoint exists or not. This simply wraps the API calls and does not do any validation. @@ -140,8 +142,8 @@ def get_client(prefix: str = "/api/", url="", token: str = "", trailing: str = " Example: >>> from chainfury import get_client >>> client = get_client() - >>> cf_actions = client.api.v1.fury.actions.list() - >>> cf_actions + >>> chains = client.api.chains() # GET /api/chains + >>> chains Note: The `get_client` function is a convenience function that can be used to get a client object. It is not required @@ -158,10 +160,10 @@ def get_client(prefix: str = "/api/", url="", token: str = "", trailing: str = " Returns: Subway: A Subway object that can be used to interact with the API. """ - url = url or os.environ.get("CF_URL", "") + url = url or CFEnv.CF_URL() if not url: raise ValueError("No url provided, please set CF_URL environment variable or pass url as argument") - token = token or os.environ.get("CF_TOKEN", "") + token = token or CFEnv.CF_TOKEN() if not token: raise ValueError("No token provided, please set CF_TOKEN environment variable or pass token as argument") diff --git a/chainfury/utils.py b/chainfury/utils.py index 625a60a3..930c8d81 100644 --- a/chainfury/utils.py +++ b/chainfury/utils.py @@ -21,6 +21,8 @@ class CFEnv: * CF_BLOB_BUCKET: blob storage bucket name (only used for `s3` engine) * CF_BLOB_PREFIX: blob storage prefix (only used for `s3` engine) * CF_BLOB_AWS_CLOUD_FRONT: blob storage cloud front url, if not provided defaults to primary S3 URL (only used for `s3` engine) + * CF_URL: the URL of the chainfury server + * CF_TOKEN: the token to use to authenticate with the chainfury server """ CF_LOG_LEVEL = lambda: os.getenv("CF_LOG_LEVEL", "info") @@ -30,6 +32,8 @@ class CFEnv: CF_BLOB_BUCKET = lambda: os.getenv("CF_BLOB_BUCKET", "") CF_BLOB_PREFIX = lambda: os.getenv("CF_BLOB_PREFIX", "") CF_BLOB_AWS_CLOUD_FRONT = lambda: os.getenv("CF_BLOB_AWS_CLOUD_FRONT", "") + CF_URL = lambda: os.getenv("CF_URL", "") + CF_TOKEN = lambda: os.getenv("CF_TOKEN", "") def store_blob(key: str, value: bytes, engine: str = "", bucket: str = "") -> str: From a1d74f47ce8046437ac32cb42c97876ec2ebe1a4 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Tue, 17 Oct 2023 00:22:51 +0530 Subject: [PATCH 07/14] [cfi] add fallbacks --- cf_internal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf_internal b/cf_internal index 9d04a229..158b45f0 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit 9d04a22930d6a4c3ea33e625f64d5aed76d174a5 +Subproject commit 158b45f0016bbee614089d26348504979683db6e From e5d2c73ed623d0fe08e9e44f842f612647c6a1e6 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Wed, 18 Oct 2023 15:11:47 +0530 Subject: [PATCH 08/14] [cfi] speed up by hashing --- cf_internal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf_internal b/cf_internal index 158b45f0..d6e43f1f 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit 158b45f0016bbee614089d26348504979683db6e +Subproject commit d6e43f1fb0c809df3bcab15b34ec091b41892c45 From aacbb707dc2fb41d5bdcb69de03d4cccabe523ae Mon Sep 17 00:00:00 2001 From: yashbonde Date: Fri, 20 Oct 2023 11:56:44 +0530 Subject: [PATCH 09/14] [cfi] new searching chain --- cf_internal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf_internal b/cf_internal index d6e43f1f..6d40af97 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit d6e43f1fb0c809df3bcab15b34ec091b41892c45 +Subproject commit 6d40af97e2e48d7a84f95b5e3a8ca426314a3e45 From 3f223cea43e1f292111ff9bd31282cc5ff28b9b1 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Sat, 21 Oct 2023 13:23:26 +0530 Subject: [PATCH 10/14] [cf-server] modify to latest DB schema --- chainfury/types.py | 2 +- chainfury/utils.py | 37 ++++++++++++ client/src/pages/Dashboard/index.tsx | 3 +- server/chainfury_server/api/chains.py | 12 +++- server/chainfury_server/api/prompts.py | 8 +-- server/chainfury_server/api/user.py | 2 +- server/chainfury_server/database.py | 82 +++++++++++++------------- 7 files changed, 97 insertions(+), 49 deletions(-) diff --git a/chainfury/types.py b/chainfury/types.py index 10de027f..768f05f0 100644 --- a/chainfury/types.py +++ b/chainfury/types.py @@ -149,4 +149,4 @@ class ApiChangePassword(BaseModel): class ApiPromptFeedback(BaseModel): - score: str + score: int diff --git a/chainfury/utils.py b/chainfury/utils.py index 930c8d81..dee3f3d8 100644 --- a/chainfury/utils.py +++ b/chainfury/utils.py @@ -1,4 +1,5 @@ import os +import re import json import time import time @@ -200,6 +201,42 @@ def exponential_backoff(foo, *args, max_retries=2, retry_delay=1, **kwargs) -> D raise Exception("This should never happen") +""" +File System +""" + + +def get_files_in_folder( + folder, + ext=["*"], + ig_pat: str = "", + abs_path: bool = True, + followlinks: bool = False, +) -> List[str]: + """Get files with `ext` in `folder`""" + # this method is faster than glob + all_paths = [] + _all = "*" in ext # wildcard means everything so speed up + ignore_pat = re.compile(ig_pat) + + folder_abs = os.path.abspath(folder) if abs_path else folder + for root, _, files in os.walk(folder_abs, followlinks=followlinks): + if _all: + for f in files: + _fp = joinp(root, f) + if not ignore_pat.search(_fp): + all_paths.append(_fp) + continue + + for f in files: + for e in ext: + if f.endswith(e): + _fp = joinp(root, f) + if not ignore_pat.search(_fp): + all_paths.append(_fp) + return all_paths + + def folder(x: str) -> str: """get the folder of this file path""" return os.path.split(os.path.abspath(x))[0] diff --git a/client/src/pages/Dashboard/index.tsx b/client/src/pages/Dashboard/index.tsx index f8fbaf84..0749b8ca 100644 --- a/client/src/pages/Dashboard/index.tsx +++ b/client/src/pages/Dashboard/index.tsx @@ -108,6 +108,7 @@ const Dashboard = () => { Edit */}
+ {auth?.selectedChatBot?.description ?? 'Description comes here'}
{chainMetrics ? ( {
Clicking on bottom chat icon to run {auth?.selectedChatBot?.name} - Or embed the bot on your website by adding the following code to your HTML + or embed the bot on your website by adding the following code to your HTML.
Date: Tue, 24 Oct 2023 03:22:17 +0530 Subject: [PATCH 11/14] [cfi] update + batched in cf utils --- .gitignore | 1 - api_docs/conf.py | 2 +- api_docs/examples/agent-theory.rst | 56 +++ api_docs/examples/qa-rag.rst | 4 +- api_docs/index.rst | 6 + cf_internal | 2 +- chainfury/agent.py | 1 + chainfury/components/nbx/__init__.py | 113 ----- chainfury/components/stability/__init__.py | 2 +- chainfury/utils.py | 54 ++- chainfury/version.py | 2 +- pyproject.toml | 2 +- stories/fury.json | 481 --------------------- stories/fury_algo.py | 63 +-- stories/fury_core.py | 314 -------------- stories/fury_to_db.py | 25 -- stories/test.sh | 21 - stories/{test_core.py => test_getkv.py} | 0 18 files changed, 130 insertions(+), 1019 deletions(-) create mode 100644 api_docs/examples/agent-theory.rst delete mode 100644 chainfury/components/nbx/__init__.py delete mode 100644 stories/fury.json delete mode 100644 stories/fury_core.py delete mode 100644 stories/fury_to_db.py delete mode 100644 stories/test.sh rename stories/{test_core.py => test_getkv.py} (100%) diff --git a/.gitignore b/.gitignore index 8d215a9b..4eb73496 100644 --- a/.gitignore +++ b/.gitignore @@ -149,6 +149,5 @@ private.sh api_docs/_build/ api_docs/_static/ cf -locust_file.py demo/ logs.py diff --git a/api_docs/conf.py b/api_docs/conf.py index 951d0c4b..51ff9249 100644 --- a/api_docs/conf.py +++ b/api_docs/conf.py @@ -14,7 +14,7 @@ project = "ChainFury" copyright = "2023, NimbleBox Engineering" author = "NimbleBox Engineering" -release = "1.6.0" +release = "1.6.1" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/api_docs/examples/agent-theory.rst b/api_docs/examples/agent-theory.rst new file mode 100644 index 00000000..2afc9cd9 --- /dev/null +++ b/api_docs/examples/agent-theory.rst @@ -0,0 +1,56 @@ +Fury Agents Manifesto +===================== + +**Starting date: 21st October, 2023** + + +ChainFury's `first commit`_ was on 7th April, 2023. It has been about 6 months since then and it has undergone lot of +production usage. With multiple API changes and engines, we are now at a stable place. This is also a good time to check +up on the things that have released in the wild till now. + +tl;dr +----- + +Predictable automated chains as agents, that use tree searching algorithms to find solution to a problem with given set +of actions. Has ability to create new actions and learn from feedback. + +Agents +------ + +There have been several "agent like" systems that have been released. Some can create code, others can perform advanced +searching. Ultimately all of them can be modelled as a Chain and use different algorithms. ``chainfury`` can support +all algorithms and has a type-based robust chaining engine. This means building agents is the next logical step. There +is a lot of theory and academic research done on the topic of agents. All of them have different tradeoffs. But first +let's start with the requirements of an agent. + +* Agent should be able to execute task without human intervention +* Agent should stop when it can't proceed +* Agent should be interruptible to take in feedback +* Agent should take inputs from it's environment +* Agent should be able to remember things over time +* Agent should be predictable in its behaviour, debuggable + +Von-Neumann machine +~~~~~~~~~~~~~~~~~~~ + +We are followers of the agent as a `Von-Neumann machine`_, which means each chain has a complete I/O mechanism where +each input and output can be accessed independently. ``chainfury`` can use different memory systems like VectorDB, etc. +meaning that it can persist data over time. For the CPU analogy we have :py:mod:`chainfury.base.Chain` which models the +execution as a DAG of :py:class:`chainfury.base.Node` objects. Each node contains the unit step of the chain. We can +parallellise and speed up executions by using :py:mod:`chainfury.utils.threaded_map`. + +``chainfury`` is already being used in production and thus with the infrastructure layer sorted we can then think about +what to build on top of it. + +Automated +~~~~~~~~~ + +One of the most important things is that these agents be automated and run without human in the loop. + +Edits +----- + +.. all the links here + +.. _first commit: https://github.com/NimbleBoxAI/ChainFury/commit/64a5f7b0fcf3d8bcce0cde6ee974b659ebe01b68 +.. _Von-Neumann machine: https://blog.nimblebox.ai/new-flow-engine-from-scratch diff --git a/api_docs/examples/qa-rag.rst b/api_docs/examples/qa-rag.rst index 39edf118..b7b0d288 100644 --- a/api_docs/examples/qa-rag.rst +++ b/api_docs/examples/qa-rag.rst @@ -1,5 +1,5 @@ -Question Answering using ChainFury -================================== +(RAG) Q/A with ChainFury +======================== One of the first use cases of LLM powered apps is question answering. This is how you should think about this problem: diff --git a/api_docs/index.rst b/api_docs/index.rst index 33a9b758..03277e28 100644 --- a/api_docs/index.rst +++ b/api_docs/index.rst @@ -66,6 +66,12 @@ Read the latest blog posts: source/chainfury.components examples/components-list +.. toctree:: + :maxdepth: 2 + :caption: Research + + examples/agent-theory + .. toctree:: :maxdepth: 2 :caption: Server diff --git a/cf_internal b/cf_internal index 6d40af97..ef45bcff 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit 6d40af97e2e48d7a84f95b5e3a8ca426314a3e45 +Subproject commit ef45bcff3e9b3e20d942ff739820f83db0957e55 diff --git a/chainfury/agent.py b/chainfury/agent.py index a082e976..842e1be7 100644 --- a/chainfury/agent.py +++ b/chainfury/agent.py @@ -21,6 +21,7 @@ Node, Model, Var, + Chain, ) # Models diff --git a/chainfury/components/nbx/__init__.py b/chainfury/components/nbx/__init__.py deleted file mode 100644 index 83054167..00000000 --- a/chainfury/components/nbx/__init__.py +++ /dev/null @@ -1,113 +0,0 @@ -import random -import requests -from typing import Any, List, Optional - -from chainfury import Secret, model_registry, exponential_backoff, Model, UnAuthException -from chainfury.components.const import Env - - -def nbx_chat_api( - inputs: str, - nbx_deploy_url: str = "", - nbx_header_token: Secret = Secret(""), - best_of: int = 1, - decoder_input_details: bool = True, - details: bool = True, - do_sample: bool = True, - max_new_tokens: int = 20, - repetition_penalty: float = 1.03, - return_full_text: bool = False, - seed: int = None, # type: ignore # see components README.md - stop: List[str] = [], - temperature: float = 0.5, - top_k: int = 10, - top_p: float = 0.95, - truncate: int = None, # type: ignore # see components README.md - typical_p: float = 0.95, - watermark: bool = True, - *, - retry_count: int = 3, - retry_delay: int = 1, -) -> Any: - """ - Returns a JSON object containing the OpenAI's API chat response. - - Args: - inputs (str): The prompt to send to the API. - nbx_deploy_url (str): The NBX deploy URL. Defaults to the value of NBX_DEPLOY_URL environment variable. - nbx_header_token (Secret): The NBX header token. Defaults to the value of NBX_DEPLOY_KEY environment variable. - best_of (int): The number of outputs to generate and return. Defaults to 1. - decoder_input_details (bool): Whether to return the decoder input details. Defaults to True. - details (bool): Whether to return the details. Defaults to True. - do_sample (bool): Whether to use sampling. Defaults to True. - max_new_tokens (int): The maximum number of tokens to generate. Defaults to 20. - repetition_penalty (float): The repetition penalty. Defaults to 1.03. - return_full_text (bool): Whether to return the full text. Defaults to False. - seed (int): The seed to use for random number generation. Defaults to a random integer between 0 and 2^32 - 1. - stop (List[str]): The stop tokens. Defaults to an empty list. - temperature (float): The temperature. Defaults to 0.5. - top_k (int): The top k. Defaults to 10. - top_p (float): The top p. Defaults to 0.95. - truncate (int): The truncate. Defaults to None. - typical_p (float): The typical p. Defaults to 0.95. - watermark (bool): Whether to include the watermark. Defaults to True. - retry_count (int): The number of times to retry the API call. Defaults to 3. - retry_delay (int): The number of seconds to wait between retries. Defaults to 1. - - Returns: - Any: The JSON object containing the OpenAI's API chat response. - """ - if not nbx_deploy_url: - nbx_deploy_url = Env.NBX_DEPLOY_URL("") - if not nbx_deploy_url: - raise Exception("NBX_DEPLOY_URL not set, please set it in your environment or pass it as an argument") - - if not nbx_header_token: - nbx_header_token = Secret(Env.NBX_DEPLOY_KEY("")).value # type: ignore - if not nbx_header_token: - raise Exception("NBX_DEPLOY_KEY not set, please set it in your environment or pass it as an argument") - - seed = seed or random.randint(0, 2**32 - 1) - - def _fn(): - r = requests.post( - nbx_deploy_url + "/generate", - headers={"NBX-KEY": nbx_header_token}, - json={ - "inputs": inputs, - "parameters": { - "best_of": best_of, - "decoder_input_details": decoder_input_details, - "details": details, - "do_sample": do_sample, - "max_new_tokens": max_new_tokens, - "repetition_penalty": repetition_penalty, - "return_full_text": return_full_text, - "seed": seed, - "stop": stop, - "temperature": temperature, - "top_k": top_k, - "top_p": top_p, - "truncate": truncate, - "typical_p": typical_p, - "watermark": watermark, - }, - }, - ) - if r.status_code == 401: - raise UnAuthException(r.text) - if r.status_code != 200: - raise Exception(f"OpenAI API returned status code {r.status_code}: {r.text}") - return r.json() - - return exponential_backoff(_fn, max_retries=retry_count, retry_delay=retry_delay) - - -model_registry.register( - model=Model( - collection_name="nbx", - id="nbx-deploy", - fn=nbx_chat_api, - description="Call NimbleBox LLMOps deploy API", - ), -) diff --git a/chainfury/components/stability/__init__.py b/chainfury/components/stability/__init__.py index c57d9575..ace15242 100644 --- a/chainfury/components/stability/__init__.py +++ b/chainfury/components/stability/__init__.py @@ -5,7 +5,7 @@ You need to have `stability_sdk` installed to use this component. You can install it with: .. code-block:: bash - + pip install chainfury[stability] # or to install all the components, note this will keep on growing pip install chainfury[all] diff --git a/chainfury/utils.py b/chainfury/utils.py index dee3f3d8..7c59d0e6 100644 --- a/chainfury/utils.py +++ b/chainfury/utils.py @@ -144,7 +144,10 @@ def get_logger() -> logging.Logger: logger.setLevel(getattr(logging, lvl)) log_handler = logging.StreamHandler() log_handler.setFormatter( - logging.Formatter("[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s", datefmt="%Y-%m-%dT%H:%M:%S%z") + logging.Formatter( + "[%(asctime)s] [%(levelname)s] [%(filename)s:%(lineno)d] %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S%z", + ) ) logger.addHandler(log_handler) return logger @@ -208,7 +211,7 @@ def exponential_backoff(foo, *args, max_retries=2, retry_delay=1, **kwargs) -> D def get_files_in_folder( folder, - ext=["*"], + ext="*", ig_pat: str = "", abs_path: bool = True, followlinks: bool = False, @@ -216,6 +219,7 @@ def get_files_in_folder( """Get files with `ext` in `folder`""" # this method is faster than glob all_paths = [] + ext = [ext] if isinstance(ext, str) else ext _all = "*" in ext # wildcard means everything so speed up ignore_pat = re.compile(ig_pat) @@ -252,7 +256,9 @@ def joinp(x: str, *args) -> str: """ -def threaded_map(fn, inputs: List[Tuple[Any]], wait: bool = True, max_threads=20, _name: str = "") -> Union[Dict[Future, int], List[Any]]: +def threaded_map( + fn, inputs: List[Tuple], wait: bool = True, max_threads=20, post_fn=None, _name: str = "" +) -> Union[Dict[Future, int], List[Any]]: """ inputs is a list of tuples, each tuple is the input for single invocation of fn. order is preserved. @@ -261,6 +267,7 @@ def threaded_map(fn, inputs: List[Tuple[Any]], wait: bool = True, max_threads=20 inputs (List[Tuple[Any]]): All the inputs to the function, can be a generator wait (bool, optional): If true, wait for all the threads to finish, otherwise return a dict of futures. Defaults to True. max_threads (int, optional): The maximum number of threads to use. Defaults to 20. + post_fn (function, optional): A function to call with the result. Defaults to None. _name (str, optional): The name of the thread pool. Defaults to "". """ _name = _name or str(uuid4()) @@ -273,12 +280,48 @@ def threaded_map(fn, inputs: List[Tuple[Any]], wait: bool = True, max_threads=20 for future in as_completed(futures): try: i, res = future.result() + if post_fn: + res = post_fn(res) results[i] = res except Exception as e: raise e return results +def batched(iterable, n): + """Convert any ``iterable`` to a generator of batches of size ``n``, last one may be smaller. + Python 3.12 has ``itertools.batched`` which does the same thing. + + Example: + >>> for x in batched(range(10), 3): + ... print(x) + [0, 1, 2] + [3, 4, 5] + [6, 7, 8] + [9] + + Args: + iterable (Iterable): The iterable to convert to batches + n (int): The batch size + + Yields: + Iterator: The batched iterator + """ + done = False + buffer = [] + _iter = iter(iterable) + while not done: + try: + buffer.append(next(_iter)) + if len(buffer) == n: + yield buffer + buffer = [] + except StopIteration: + done = True + if buffer: + yield buffer + + """ Ser/Deser """ @@ -326,6 +369,11 @@ def from_json(fp: str = "") -> Dict[str, Any]: return json.loads(fp) +""" +Time management should be dead easy. +""" + + class SimplerTimes: """ A class that provides a simpler interface to datetime and time modules. diff --git a/chainfury/version.py b/chainfury/version.py index 7831c4b2..00777212 100644 --- a/chainfury/version.py +++ b/chainfury/version.py @@ -1,4 +1,4 @@ -__version__ = "1.6.0" +__version__ = "1.6.1" _major, _minor, _patch = __version__.split(".") _major = int(_major) _minor = int(_minor) diff --git a/pyproject.toml b/pyproject.toml index 942211e2..fa651942 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chainfury" -version = "1.6.0" +version = "1.6.1" description = "ChainFury is a powerful tool that simplifies the creation and management of chains of prompts, making it easier to build complex chat applications using LLMs." authors = ["NimbleBox Engineering "] license = "Apache 2.0" diff --git a/stories/fury.json b/stories/fury.json deleted file mode 100644 index 302d3a3f..00000000 --- a/stories/fury.json +++ /dev/null @@ -1,481 +0,0 @@ -{ - "nodes": [ - { - "id": "find-quote", - "type": "ai-powered", - "fn": { - "node_id": "find-quote", - "model": { - "collection_name": "openai", - "model_id": "openai-chat", - "description": "Given a list of messages describing a conversation, the model will return a response.", - "tags": [], - "vars": [] - }, - "model_params": { - "model": "gpt-3.5-turbo" - }, - "fn": { - "messages": [ - { - "role": "user", - "content": "'{{ quote }}' \nWho said this quote, if you don't know then reply with a random character from history world? Give reply in less than 10 words." - } - ] - }, - "action_source": "jinja-template" - }, - "description": "", - "fields": [ - { - "type": "string", - "required": true, - "name": "quote" - }, - { - "type": "string", - "password": true, - "required": true, - "show": true, - "name": "openai_api_key" - }, - { - "type": "string", - "required": true, - "show": true, - "name": "model" - }, - { - "type": "array", - "items": [ - { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - ], - "required": true, - "show": true, - "name": "messages" - }, - { - "type": "number", - "placeholder": "1.0", - "show": true, - "name": "temperature" - }, - { - "type": "number", - "placeholder": "1.0", - "show": true, - "name": "top_p" - }, - { - "type": "number", - "placeholder": "1", - "show": true, - "name": "n" - }, - { - "type": [ - { - "type": "string" - }, - { - "type": "array", - "items": [ - { - "type": "string" - } - ] - } - ], - "show": true, - "name": "stop" - }, - { - "type": "number", - "placeholder": "1024", - "show": true, - "name": "max_tokens" - }, - { - "type": "number", - "placeholder": "0.0", - "show": true, - "name": "presence_penalty" - }, - { - "type": "number", - "placeholder": "0.0", - "show": true, - "name": "frequency_penalty" - }, - { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "placeholder": "{}", - "show": true, - "name": "logit_bias" - }, - { - "type": "string", - "show": true, - "name": "user" - }, - { - "type": "boolean", - "placeholder": "False", - "show": true, - "name": "raw" - } - ], - "outputs": [ - { - "type": "any", - "name": "chat_reply", - "loc": [ - "choices", - 0, - "message", - "content" - ] - } - ] - }, - { - "id": "tell-character-story", - "type": "ai-powered", - "fn": { - "node_id": "tell-character-story", - "model": { - "collection_name": "openai", - "model_id": "openai-chat", - "description": "Given a list of messages describing a conversation, the model will return a response.", - "tags": [], - "vars": [] - }, - "model_params": { - "model": "gpt-3.5-turbo" - }, - "fn": { - "messages": [ - { - "role": "user", - "content": "Tell a small {{ story_size }} line story about '{{ character_name }}'" - } - ] - }, - "action_source": "jinja-template" - }, - "description": "", - "fields": [ - { - "type": "string", - "required": true, - "name": "story_size" - }, - { - "type": "string", - "required": true, - "name": "character_name" - }, - { - "type": "string", - "password": true, - "required": true, - "show": true, - "name": "openai_api_key" - }, - { - "type": "string", - "required": true, - "show": true, - "name": "model" - }, - { - "type": "array", - "items": [ - { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - ], - "required": true, - "show": true, - "name": "messages" - }, - { - "type": "number", - "placeholder": "1.0", - "show": true, - "name": "temperature" - }, - { - "type": "number", - "placeholder": "1.0", - "show": true, - "name": "top_p" - }, - { - "type": "number", - "placeholder": "1", - "show": true, - "name": "n" - }, - { - "type": [ - { - "type": "string" - }, - { - "type": "array", - "items": [ - { - "type": "string" - } - ] - } - ], - "show": true, - "name": "stop" - }, - { - "type": "number", - "placeholder": "1024", - "show": true, - "name": "max_tokens" - }, - { - "type": "number", - "placeholder": "0.0", - "show": true, - "name": "presence_penalty" - }, - { - "type": "number", - "placeholder": "0.0", - "show": true, - "name": "frequency_penalty" - }, - { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "placeholder": "{}", - "show": true, - "name": "logit_bias" - }, - { - "type": "string", - "show": true, - "name": "user" - }, - { - "type": "boolean", - "placeholder": "False", - "show": true, - "name": "raw" - } - ], - "outputs": [ - { - "type": "any", - "name": "characters_story", - "loc": [ - "choices", - 0, - "message", - "content" - ] - } - ] - }, - { - "id": "deep-rap-quote", - "type": "ai-powered", - "fn": { - "node_id": "deep-rap-quote", - "model": { - "collection_name": "openai", - "model_id": "openai-chat", - "description": "Given a list of messages describing a conversation, the model will return a response.", - "tags": [], - "vars": [] - }, - "model_params": { - "model": "gpt-3.5-turbo" - }, - "fn": { - "messages": [ - { - "role": "user", - "content": "give a deep 8 line rap quote on life in the style of {{ character }}." - } - ] - }, - "action_source": "jinja-template" - }, - "description": "AI will tell a joke on any topic you tell it to talk about", - "fields": [ - { - "type": "string", - "required": true, - "name": "character" - }, - { - "type": "string", - "password": true, - "required": true, - "show": true, - "name": "openai_api_key" - }, - { - "type": "string", - "required": true, - "show": true, - "name": "model" - }, - { - "type": "array", - "items": [ - { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - ], - "required": true, - "show": true, - "name": "messages" - }, - { - "type": "number", - "placeholder": "1.0", - "show": true, - "name": "temperature" - }, - { - "type": "number", - "placeholder": "1.0", - "show": true, - "name": "top_p" - }, - { - "type": "number", - "placeholder": "1", - "show": true, - "name": "n" - }, - { - "type": [ - { - "type": "string" - }, - { - "type": "array", - "items": [ - { - "type": "string" - } - ] - } - ], - "show": true, - "name": "stop" - }, - { - "type": "number", - "placeholder": "1024", - "show": true, - "name": "max_tokens" - }, - { - "type": "number", - "placeholder": "0.0", - "show": true, - "name": "presence_penalty" - }, - { - "type": "number", - "placeholder": "0.0", - "show": true, - "name": "frequency_penalty" - }, - { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "placeholder": "{}", - "show": true, - "name": "logit_bias" - }, - { - "type": "string", - "show": true, - "name": "user" - }, - { - "type": "boolean", - "placeholder": "False", - "show": true, - "name": "raw" - } - ], - "outputs": [ - { - "type": "any", - "name": "chat_reply", - "loc": [ - "choices", - 0, - "message", - "content" - ] - } - ] - } - ], - "edges": [ - { - "src_node_id": "find-quote", - "trg_node_id": "tell-character-story", - "connections": [ - [ - "chat_reply", - "character_name" - ] - ] - }, - { - "src_node_id": "tell-character-story", - "trg_node_id": "deep-rap-quote", - "connections": [ - [ - "characters_story", - "character" - ] - ] - } - ], - "topo_order": [ - "find-quote", - "tell-character-story", - "deep-rap-quote" - ], - "sample": { - "openai_api_key": "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", - "quote": "hello there nice world", - "story_size": 2 - }, - "main_in": "quote", - "main_out": "deep-rap-quote/chat_reply" -} \ No newline at end of file diff --git a/stories/fury_algo.py b/stories/fury_algo.py index 84c702c5..3c4f985a 100644 --- a/stories/fury_algo.py +++ b/stories/fury_algo.py @@ -1,37 +1,10 @@ # what are some interesting algorithms that we can build using fury? -import os -import json +import re import fire from pprint import pformat -from requests import Session -from typing import Dict, Any -from chainfury import ( - Chain, - programatic_actions_registry, - model_registry, - Node, - ai_actions_registry, - Edge, -) - - -def _get_openai_token() -> str: - openai_token = os.environ.get("OPENAI_TOKEN", "") - if not openai_token: - raise ValueError("OpenAI token not found") - return openai_token - - -def _get_nbx_token() -> Dict[str, str]: - nbx_token = os.environ.get("NBX_DEPLOY_KEY", "") - if not nbx_token: - raise ValueError("NBX token not found") - nbx_url = os.environ.get("NBX_DEPLOY_URL", "") - if not nbx_url: - raise ValueError("NBX url not found") - return {"nbx_deploy_url": nbx_url, "nbx_header_token": nbx_token} +from chainfury import Chain, ai_actions_registry, Edge class Actions: @@ -203,21 +176,21 @@ class Actions: class Chains: story = Chain( [Actions.sensational_story], - sample={"openai_api_key": _get_openai_token(), "scene": ""}, + sample={"scene": ""}, main_in="scene", main_out=f"{Actions.sensational_story.id}/story", ) # type: ignore story_nbx = Chain( [Actions.sensational_story_nbx], - sample={"scene": "", **_get_nbx_token()}, + sample={"scene": ""}, main_in="scene", main_out=f"{Actions.sensational_story_nbx.id}/story", ) # type: ignore feedback = Chain( [Actions.people_feedback], - sample={"openai_api_key": _get_openai_token(), "story": ""}, + sample={"story": ""}, main_in="story", main_out=f"{Actions.people_feedback.id}/story_accepted", ) # type: ignore @@ -227,7 +200,7 @@ class Chains: [ Edge(Actions.sensational_story.id, "story", Actions.catchy_headline.id, "story"), ], - sample={"openai_api_key": _get_openai_token(), "scene": ""}, + sample={"scene": ""}, main_in="scene", main_out=f"{Actions.catchy_headline.id}/headline", ) @@ -238,7 +211,7 @@ class Chains: Edge(Actions.topic_to_synopsis.id, "synopsis", Actions.sensational_story.id, "scene"), Edge(Actions.sensational_story.id, "story", Actions.catchy_headline.id, "story"), ], - sample={"openai_api_key": _get_openai_token(), "topics": ""}, + sample={"topics": ""}, main_in="topics", main_out=f"{Actions.catchy_headline.id}/headline", ) @@ -256,7 +229,7 @@ class Chains: Edge(Actions.catchy_headline.id, "headline", Actions.sensational_story_generator.id, "headline"), Edge(Actions.topic_to_synopsis.id, "synopsis", Actions.sensational_story_generator.id, "sub_headline"), ], - sample={"openai_api_key": _get_openai_token(), "topics": ""}, + sample={"topics": ""}, main_in="topics", main_out=f"{Actions.catchy_headline.id}/headline", ) @@ -264,7 +237,7 @@ class Chains: good_story = Chain( [Actions.sensational_story, Actions.corrupt_editor_check], [Edge(Actions.sensational_story.id, "story", Actions.corrupt_editor_check.id, "story")], # type: ignore - sample={"openai_api_key": _get_openai_token(), "scene": ""}, + sample={"scene": ""}, main_in="scene", main_out=f"{Actions.corrupt_editor_check.id}/story_accepted", ) # type: ignore @@ -289,16 +262,6 @@ def chain_of_thought(scene: str, v: bool = False): return out -def chain_of_thought_topic(topics: str, v: bool = False): - if isinstance(topics, tuple): - topics = ", ".join(topics) - out, thoughts = Chains.topic_to_story(topics) # type: ignore - if v: - print("BUFF:", pformat(thoughts)) - print(" OUT:", out) - return out - - # self consistency with CoT (CoT-SC) # https://arxiv.org/pdf/2203.11171.pdf def cot_consistency(scene, n: int = 3, v: bool = False, pb: bool = False): @@ -334,8 +297,6 @@ def __init__(self, max_search_space: int = 5): self.value_fn = Chains.feedback def __call__(self, topics: str, v: bool = False): - import re - done = False total_searches = 0 result = None @@ -387,14 +348,8 @@ def tree_of_thought(topics: str, max_search_space: int = 5, v: bool = False): "algo": { "io": io_prompting, "cot": chain_of_thought, - "cot_t": chain_of_thought_topic, "cot-sc": cot_consistency, "tot": tree_of_thought, }, - "chains": Chains, - "print": { - "action": lambda x: print(getattr(Actions, x).to_json()), - "chain": lambda x: print(getattr(Chains, x).to_json()), - }, } ) diff --git a/stories/fury_core.py b/stories/fury_core.py deleted file mode 100644 index 4895f8f4..00000000 --- a/stories/fury_core.py +++ /dev/null @@ -1,314 +0,0 @@ -import os -import json -import fire -from pprint import pformat -from requests import Session -from typing import Dict, Any - -from chainfury import ( - Chain, - programatic_actions_registry, - model_registry, - Node, - ai_actions_registry, - Edge, -) - - -def _get_openai_token() -> str: - openai_token = os.environ.get("OPENAI_TOKEN", "") - if not openai_token: - raise ValueError("OpenAI token not found") - return openai_token - - -def _get_cf_token() -> str: - cf_token = os.environ.get("CF_TOKEN", "") - if not cf_token: - raise ValueError("CF_TOKEN token not found") - return cf_token - - -class _Nodes: - def callp(self, fail: bool = False): - """Call a programatic action""" - node = programatic_actions_registry.get("call_api_requests") - print("NODE:", node) - data = { - "method": "get", - "url": "http://127.0.0.1:8000/api/v1/fury/components/", - "headers": {"token": _get_cf_token()}, - } - if fail: - data["some-key"] = "some-value" - out, err = node(data) - if err: - print("ERROR:", err) - print("TRACE:", out) - return - print("OUT:", out) - - def callm(self, fail: bool = False): - """Call a model""" - model = model_registry.get("openai-completion") - print("Found model:", model) - data = { - "openai_api_key": _get_openai_token(), - "model": "text-curie-001", - "prompt": "What comes after 0,1,1,2?", - } - if fail: - data["model"] = "this-does-not-exist" - out, err = model(data) - if err: - print("ERROR:", err) - print("TRACE:", out) - return - print("OUT:", out) - - def callai(self, fail: bool = False): - """Call the AI action""" - if fail: - action_id = "write-a-poem" - else: - action_id = "hello-world" - action = ai_actions_registry.get(action_id) - # print(action) - - out, err = action( - { - "openai_api_key": _get_openai_token(), - "message": "hello world", - "temperature": 0.12, - # "style": "snoop dogg", # uncomment to get the fail version running correctly - } - ) - if err: - print("ERROR:", err) - print("TRACE:", out) - return - print("OUT:", out) - - def callai_chat(self, character: str = "a mexican taco"): - """Call the AI action""" - action_id = "deep-rap-quote" - action = ai_actions_registry.get(action_id) - print("ACTION:", action) - - out, err = action( - { - "openai_api_key": _get_openai_token(), - "character": character, - }, - ) # type: ignore - if err: - print("ERROR:", err) - print("TRACE:", out) - return - print("OUT:", out) - - -class _Chain: - def callpp(self): - p1 = programatic_actions_registry.get("call_api_requests") - p2 = programatic_actions_registry.get("regex_substitute") - e = Edge(p1.id, "text", p2.id, "text") # type: ignore - c = Chain([p1, p2], [e], sample={"url": ""}, main_in="url", main_out=f"{p2.id}/text") # type: ignore - print("CHAIN:", c) - - # run the chain - out, full_ir = c( - { - "method": "GET", - "url": "http://127.0.0.1:8000/api/v1/fury/", - "headers": {"token": _get_cf_token()}, - "pattern": "JWT", - "repl": "booboo-hooooo", - }, - ) - print("BUFF:", pformat(full_ir)) - print("OUT:", pformat(out)) - - def callpj(self, fail: bool = False): - p = programatic_actions_registry.get("call_api_requests") - - # create a new ai action to build a poem - NODE_ID = "sarcastic-agent" - j = ai_actions_registry.register( - node_id=NODE_ID, - description="AI will add two numbers and give a sarscastic response. J-type action", - model_id="openai-chat", - model_params={ - "model": "gpt-3.5-turbo", - }, - fn={ - "messages": [ - { - "role": "user", - "content": "Hello there, can you add these two numbers for me? 1023, 97. Be super witty in all responses.", - }, - { - "role": "assistant", - "content": "It is 1110. WTF I mean I am a powerful AI, I have better things to do!", - }, - { - "role": "user", - "content": "Can you explain this json to me? {{ json_thingy }}", - }, - ], - }, - outputs={ - "chat_reply": ("choices", 0, "message", "content"), - }, - ) - print("ACTION:", j) - - e = Edge(p.id, "text", j.id, "json_thingy") - - c = Chain( - [p, j], - [e], - sample={ - "method": "GET", - }, - main_in="url", - main_out=f"{j.id}/chat_reply", - ) - print("CHAIN:", c) - - # run the chain - out, full_ir = c( - { - "method": "get", - "url": "http://127.0.0.1:8000/api/v1/fury/", - "headers": {"token": _get_cf_token()}, - "openai_api_key": _get_openai_token(), - } - ) - print("BUFF:", pformat(full_ir)) - print("OUT:", pformat(out)) - - def calljj(self): - j1 = ai_actions_registry.get("hello-world") - print("ACTION:", j1) - j2 = ai_actions_registry.get("deep-rap-quote") - print("ACTION:", j2) - e = Edge(j1.id, "generations", j2.id, "character") - c = Chain([j1, j2], [e], sample={"message": "hello world"}, main_in="message", main_out=f"{j2.id}/chat_reply") - print("CHAIN:", c) - - # run the chain - out, full_ir = c( - { - "openai_api_key": _get_openai_token(), - "message": "hello world", - } - ) - print("BUFF:", pformat(full_ir)) - print("OUT:", pformat(out)) - - def callj3(self, quote: str, n: int = 4, thoughts: bool = False, to_json: bool = False): - findQuote = ai_actions_registry.register( - node_id="find-quote", - model_id="openai-chat", - model_params={ - "model": "gpt-3.5-turbo", - }, - fn={ - "messages": [ - { - "role": "user", - "content": "'{{ quote }}' \nWho said this quote, if you don't know then reply with a random character from history world? Give reply in less than 10 words.", - }, - ], - }, - outputs={ - "chat_reply": ("choices", 0, "message", "content"), - }, - ) - - charStory = ai_actions_registry.register( - node_id="tell-character-story", - model_id="openai-chat", - model_params={ - "model": "gpt-3.5-turbo", - }, - fn={ - "messages": [ - {"role": "user", "content": "Tell a small {{ story_size }} line story about '{{ character_name }}'"}, - ], - }, - outputs={ - "characters_story": ("choices", 0, "message", "content"), - }, - ) - rapMaker = ai_actions_registry.get("deep-rap-quote") - e1 = Edge(findQuote.id, "chat_reply", charStory.id, "character_name") - e2 = Edge(charStory.id, "characters_story", rapMaker.id, "character") - c = Chain( - [findQuote, charStory, rapMaker], - [e1, e2], - sample={"quote": quote}, - main_in="quote", - main_out=f"{rapMaker.id}/chat_reply", - ) - print("CHAIN:", c) - - sample_input = {"openai_api_key": _get_openai_token(), "quote": quote, "story_size": n} # these will also act like defaults - # sample_input = {"quote": quote, "story_size": n} # these will also act like defaults - - if to_json: - print(json.dumps(c.to_dict("quote", f"{rapMaker.id}/chat_reply", sample_input), indent=2)) - return - - # run the chain - sample_input["openai_api_key"] = _get_openai_token() - out, full_ir = c( - sample_input, - print_thoughts=thoughts, - ) - - print("BUFF:", pformat(full_ir)) - print("OUT:", pformat(out)) - - def from_json(self, quote: str = "", n: int = 4, mainline: bool = False, thoughts: bool = False, path: str = "./stories/fury.json"): - with open(path) as f: - dag = json.load(f) - c = Chain.from_dict(dag) - print("CHAIN:", c) - - if mainline: - input = quote - else: - # run the chain - input = {"openai_api_key": _get_openai_token()} - if quote: - input["quote"] = quote - if n: - input["story_size"] = n - out, full_ir = c( - input, - print_thoughts=thoughts, - ) - print("BUFF:", pformat(full_ir)) - print("OUT:", pformat(out)) - - -if __name__ == "__main__": - - def help(): - return """ -Fury Story -========== - -python3 -m stories.fury nodes callp [--fail] -python3 -m stories.fury nodes callai [--jtype --fail] -python3 -m stories.fury nodes callai_chat [--jtype --fail] - -python3 -m stories.fury chain callpp -python3 -m stories.fury chain callpj -python3 -m stories.fury chain calljj -python3 -m stories.fury chain callj3 --quote QUOTE -""".strip() - - fire.Fire({"nodes": _Nodes, "chain": _Chain, "help": help}) diff --git a/stories/fury_to_db.py b/stories/fury_to_db.py deleted file mode 100644 index 93b939b0..00000000 --- a/stories/fury_to_db.py +++ /dev/null @@ -1,25 +0,0 @@ -# from chainfury import ai_actions_registry, cf_client - -# make an action -sensational_story = ai_actions_registry.to_action( - name="sensational_story", - model_id="openai-chat", - model_params={ - "model": "gpt-3.5-turbo", - }, - fn={ - "messages": [ - { - "role": "user", - "content": "You are a Los Santos correspondent and saw '{{ scene }}'. Make it into a small 6 line witty, sarcastic, funny sensational story as if you are on Radio Mirror Park.", - }, - ], - }, - outputs={ - "story": ("choices", 0, "message", "content"), - }, -) - -# -# sensational_story = cf_client.get_or_create_node(sensational_story) -out = sensational_story("there are times, when I don't know what to do!") diff --git a/stories/test.sh b/stories/test.sh deleted file mode 100644 index b72a39ac..00000000 --- a/stories/test.sh +++ /dev/null @@ -1,21 +0,0 @@ -echo "######\n> python3 -m stories.test_core\n######" && python3 -m stories.test_core --verbose - -echo "######\n> python3 -m stories.fury_core nodes callm\n######" && python3 -m stories.fury_core nodes callm -echo "######\n> python3 -m stories.fury_core nodes callai\n######" && python3 -m stories.fury_core nodes callai -echo "######\n> python3 -m stories.fury_core nodes callai_chat\n######" && python3 -m stories.fury_core nodes callai_chat - -QUOTE="to great men and women who defined a new era for humanity!" - -echo "######\n> python3 -m stories.fury_core chain callpp\n######" && python3 -m stories.fury_core chain callpp -echo "######\n> python3 -m stories.fury_core chain callpj\n######" && python3 -m stories.fury_core chain callpj -echo "######\n> python3 -m stories.fury_core chain calljj\n######" && python3 -m stories.fury_core chain calljj -echo "######\n> python3 -m stories.fury_core chain callj3\n######" && python3 -m stories.fury_core chain callj3 --quote "$QUOTE" - -SCENE="ufo attacked a crow and stole 5 year olds ice cream" -TOPICS='dolphins, redbull, 5 year old' - -echo "######\n> python3 -m stories.fury_algo algo io\n######" && python3 -m stories.fury_algo algo io "$SCENE" -echo "######\n> python3 -m stories.fury_algo algo cot\n######" && python3 -m stories.fury_algo algo cot "$SCENE" -echo "######\n> python3 -m stories.fury_algo algo cot_t\n######" && python3 -m stories.fury_algo algo cot_t "$TOPICS" -echo "######\n> python3 -m stories.fury_algo algo cot-sc\n######" && python3 -m stories.fury_algo algo cot-sc "$SCENE" --n 3 -echo "######\n> python3 -m stories.fury_algo algo tot\n######" && python3 -m stories.fury_algo algo tot "$TOPICS" --max_search_space 2 diff --git a/stories/test_core.py b/stories/test_getkv.py similarity index 100% rename from stories/test_core.py rename to stories/test_getkv.py From 8a77226c0001191c22a0944584685f81920dc439 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Tue, 24 Oct 2023 03:32:47 +0530 Subject: [PATCH 12/14] [cfi] reduce chunk size --- cf_internal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf_internal b/cf_internal index ef45bcff..2fa4a502 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit ef45bcff3e9b3e20d942ff739820f83db0957e55 +Subproject commit 2fa4a5023b527067292c71fbc4bd9c5907656504 From c61e66dbb342c7197e66ddd082ff6f9f193b791b Mon Sep 17 00:00:00 2001 From: yashbonde Date: Tue, 24 Oct 2023 04:17:45 +0530 Subject: [PATCH 13/14] [cfi] fix ultra long prompt len issue --- cf_internal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cf_internal b/cf_internal index 2fa4a502..7f44e758 160000 --- a/cf_internal +++ b/cf_internal @@ -1 +1 @@ -Subproject commit 2fa4a5023b527067292c71fbc4bd9c5907656504 +Subproject commit 7f44e7587d20e09956e5dabcd6dc7af7e9019d0f From 2d503ec3a3258c5a3cf4869c28e776b1be34b312 Mon Sep 17 00:00:00 2001 From: yashbonde Date: Fri, 3 Nov 2023 11:27:59 +0530 Subject: [PATCH 14/14] [cf] 1.6.2 --- api_docs/conf.py | 2 +- chainfury/__init__.py | 17 +++++++++++-- chainfury/components/openai/__init__.py | 33 +++++++++++++++++++------ chainfury/utils.py | 24 ++++++++++++++++-- chainfury/version.py | 2 +- pyproject.toml | 2 +- 6 files changed, 66 insertions(+), 14 deletions(-) diff --git a/api_docs/conf.py b/api_docs/conf.py index 51ff9249..1da56554 100644 --- a/api_docs/conf.py +++ b/api_docs/conf.py @@ -14,7 +14,7 @@ project = "ChainFury" copyright = "2023, NimbleBox Engineering" author = "NimbleBox Engineering" -release = "1.6.1" +release = "1.6.2" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/chainfury/__init__.py b/chainfury/__init__.py index 3b197436..45ace687 100644 --- a/chainfury/__init__.py +++ b/chainfury/__init__.py @@ -5,8 +5,21 @@ if os.path.exists(_dotenv_fp): dotenv.load_dotenv(_dotenv_fp) -from chainfury.utils import exponential_backoff, UnAuthException, DoNotRetryException, logger, CFEnv +from chainfury.utils import ( + exponential_backoff, + UnAuthException, + DoNotRetryException, + logger, + CFEnv, +) from chainfury.base import Var, Node, Secret, Chain, Model, Edge -from chainfury.agent import model_registry, programatic_actions_registry, ai_actions_registry, memory_registry, AIAction, Memory +from chainfury.agent import ( + model_registry, + programatic_actions_registry, + ai_actions_registry, + memory_registry, + AIAction, + Memory, +) from chainfury.client import get_client from chainfury import components diff --git a/chainfury/components/openai/__init__.py b/chainfury/components/openai/__init__.py index 4cb48125..54e08be7 100644 --- a/chainfury/components/openai/__init__.py +++ b/chainfury/components/openai/__init__.py @@ -2,7 +2,13 @@ from pydantic import BaseModel from typing import Any, List, Union, Dict, Optional -from chainfury import Secret, model_registry, exponential_backoff, Model, UnAuthException +from chainfury import ( + Secret, + model_registry, + exponential_backoff, + Model, + UnAuthException, +) from chainfury.components.const import Env @@ -66,7 +72,9 @@ def openai_completion( if not openai_api_key: openai_api_key = Secret(Env.OPENAI_TOKEN("")).value # type: ignore if not openai_api_key: - raise Exception("OpenAI API key not found. Please set OPENAI_TOKEN environment variable or pass through function") + raise Exception( + "OpenAI API key not found. Please set OPENAI_TOKEN environment variable or pass through function" + ) def _fn(): r = requests.post( @@ -95,7 +103,9 @@ def _fn(): if r.status_code == 401: raise UnAuthException(r.text) if r.status_code != 200: - raise Exception(f"OpenAI API returned status code {r.status_code}: {r.text}") + raise Exception( + f"OpenAI API returned status code {r.status_code}: {r.text}" + ) return r.json() return exponential_backoff(_fn, max_retries=retry_count, retry_delay=retry_delay) @@ -187,7 +197,9 @@ def openai_chat( if not openai_api_key: openai_api_key = Secret(Env.OPENAI_TOKEN("")).value # type: ignore if not openai_api_key: - raise Exception("OpenAI API key not found. Please set OPENAI_TOKEN environment variable or pass through function") + raise Exception( + "OpenAI API key not found. Please set OPENAI_TOKEN environment variable or pass through function" + ) if not len(messages): raise Exception("Messages cannot be empty") @@ -214,11 +226,14 @@ def _fn(): "logit_bias": logit_bias, "user": user, }, + timeout=(5, 30), ) if r.status_code == 401: raise UnAuthException(r.text) if r.status_code != 200: - raise Exception(f"OpenAI API returned status code {r.status_code}: {r.text}") + raise Exception( + f"OpenAI API returned status code {r.status_code}: {r.text}" + ) return r.json() return exponential_backoff(_fn, max_retries=retry_count, retry_delay=retry_delay) @@ -262,7 +277,9 @@ def openai_embedding( if not openai_api_key: openai_api_key = Secret(Env.OPENAI_TOKEN("")).value # type: ignore if not openai_api_key: - raise Exception("OpenAI API key not found. Please set OPENAI_TOKEN environment variable or pass through function") + raise Exception( + "OpenAI API key not found. Please set OPENAI_TOKEN environment variable or pass through function" + ) def _fn(): r = requests.post( @@ -280,7 +297,9 @@ def _fn(): if r.status_code == 401: raise UnAuthException(r.text) if r.status_code != 200: - raise Exception(f"OpenAI API returned status code {r.status_code}: {r.text}") + raise Exception( + f"OpenAI API returned status code {r.status_code}: {r.text}" + ) return r.json() return exponential_backoff(_fn, max_retries=retry_count, retry_delay=retry_delay) diff --git a/chainfury/utils.py b/chainfury/utils.py index 7c59d0e6..5e238759 100644 --- a/chainfury/utils.py +++ b/chainfury/utils.py @@ -168,7 +168,9 @@ class DoNotRetryException(Exception): """Raised when code tells not to retry""" -def exponential_backoff(foo, *args, max_retries=2, retry_delay=1, **kwargs) -> Dict[str, Any]: +def exponential_backoff( + foo, *args, max_retries=2, retry_delay=1, **kwargs +) -> Dict[str, Any]: """Exponential backoff function Args: @@ -184,6 +186,18 @@ def exponential_backoff(foo, *args, max_retries=2, retry_delay=1, **kwargs) -> D Dict[str, Any]: The completion(s) generated by the API. """ + if not max_retries: + try: + out = foo(*args, **kwargs) # Call the function that may crash + return out # If successful, break out of the loop and return + except DoNotRetryException as e: + raise e + except UnAuthException as e: + raise e + except Exception as e: + logger.warning(f"Function crashed: {e}") + raise e + for attempt in range(max_retries): try: out = foo(*args, **kwargs) # Call the function that may crash @@ -201,6 +215,7 @@ def exponential_backoff(foo, *args, max_retries=2, retry_delay=1, **kwargs) -> D delay = retry_delay * (2**attempt) # Calculate the backoff delay logger.info(f"Retrying in {delay} seconds...") time.sleep(delay) # Wait for the calculated delay + raise Exception("This should never happen") @@ -257,7 +272,12 @@ def joinp(x: str, *args) -> str: def threaded_map( - fn, inputs: List[Tuple], wait: bool = True, max_threads=20, post_fn=None, _name: str = "" + fn, + inputs: List[Tuple], + wait: bool = True, + max_threads=20, + post_fn=None, + _name: str = "", ) -> Union[Dict[Future, int], List[Any]]: """ inputs is a list of tuples, each tuple is the input for single invocation of fn. order is preserved. diff --git a/chainfury/version.py b/chainfury/version.py index 00777212..c75a9b3a 100644 --- a/chainfury/version.py +++ b/chainfury/version.py @@ -1,4 +1,4 @@ -__version__ = "1.6.1" +__version__ = "1.6.2" _major, _minor, _patch = __version__.split(".") _major = int(_major) _minor = int(_minor) diff --git a/pyproject.toml b/pyproject.toml index fa651942..73ba85b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chainfury" -version = "1.6.1" +version = "1.6.2" description = "ChainFury is a powerful tool that simplifies the creation and management of chains of prompts, making it easier to build complex chat applications using LLMs." authors = ["NimbleBox Engineering "] license = "Apache 2.0"