diff --git a/.gitignore b/.gitignore index f39dd6c..37d64ab 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ __pycache__ python-backend/cache # venv -venv \ No newline at end of file +venv +node_modules diff --git a/CONTRIBUTOR_GUIDE.md b/CONTRIBUTOR_GUIDE.md new file mode 100644 index 0000000..b3d8109 --- /dev/null +++ b/CONTRIBUTOR_GUIDE.md @@ -0,0 +1,67 @@ +# Contributor Guide + +This is a guide to running the current version of ChainForge, for people who want to develop or extend it. +Note that this document will change in the future. + +## Getting Started +### Install requirements +Before you can run ChainForge, you need to install dependencies. `cd` into `python-backend` and run + +```bash +pip install -r requirements.txt +``` + +to install requirements. (Ideally, you will run this in a `virtualenv`.) + +To install Node.hs requirements, first make sure you have Node.js installed. Then `cd` into `chain-forge` and run: + +```bash +npm install +``` + +## Running ChainForge + +To serve ChainForge, you have two options: + 1. Run everything from a single Python script, which requires building the React app to static files, or + 2. Serve the React front-end separately from the Flask back-end and take advantage of React hot reloading. + +We recommend the former option for end-users, and the latter for developers. + +### Option 1: Build React app as static files (end-users) + +`cd` into `chain-forge` directory and run: + +``` +npm run build +``` + +Wait a moment while it builds the React app to static files. + +### Option 2: Serve React front-end with hot reloading (developers) + +`cd` into `chain-forge` directory and run the following to serve the React front-end: + +``` +npm run start +``` + +### Serving the backend + +Regardless of which option you chose, `cd` into `python-backend` and run: + +```bash +python app.py +``` + +> **Note** +> You can add the `--dummy-responses` flag in case you're worried about making calls to OpenAI. This will spoof all LLM responses as random strings, and is great for testing the interface without accidentally spending $$. + +This script spins up two servers, the main one on port 8000 and a SocketIO server on port 8001 (used for streaming progress updates). + +If you built the React app statically, go to `localhost:8000` in a web browser to view the app. +If you served the React app with hot reloading with `npm run start`, go to the server address you ran it on (usually `localhost:3000`). + + +## Contributing to ChainForge + +If you have access to the main repository, we request that you add a branch `dev/` and develop changes from there. When you are ready to push changes, say to addres an open Issue, make a Pull Request on the `main` repository and assign the main developer (Ian Arawjo) to it. diff --git a/README.md b/README.md index a71e51b..6580834 100644 --- a/README.md +++ b/README.md @@ -19,30 +19,23 @@ Taken together, these three features let you easily: # Installation -To install, use `pip`. From the command line: +To get started, currently see the `CONTRIBUTOR_GUIDE.md`. Below are the planned installation steps (which are not yet active): -``` -pip install chainforge -``` - -[TODO: Upload CF to PyPI] -[TODO: Create a command-line alias (?) so you can run `chainforge serve ` and spin up both React and the Python backend automatically.] - -To run simply, type: - -``` -chainforge serve -``` - -This spins up two local servers: a React server through npm, and a Python backend, powered by Flask. For more options, such as port numbers, type `chainforge --help`. - -### Sharing prompt chains - -All ChainForge node graphs are importable/exportable as JSON specs. You can freely share prompt chains you develop (alongside any custom analysis code), whether to the public or within your organization. +> +> To install, use `pip`. From the command line: +> +> ``` +> pip install chainforge +> ``` +> To run, type: +> ``` +> chainforge serve +> ``` +> This spins up two local servers: a React server through npm, and a Python backend, powered by Flask. For more options, such as port numbers, type `chainforge --help`. ## Example: Test LLM robustness to prompt injection -... +> ... # Development diff --git a/chain-forge/src/AlertModal.js b/chain-forge/src/AlertModal.js index 53fa4d0..1d3bd53 100644 --- a/chain-forge/src/AlertModal.js +++ b/chain-forge/src/AlertModal.js @@ -10,6 +10,7 @@ const AlertModal = forwardRef((props, ref) => { // This gives the parent access to triggering the modal alert const trigger = (msg) => { + if (!msg) msg = "Unknown error."; console.error(msg); setAlertMsg(msg); open(); diff --git a/chain-forge/src/App.css b/chain-forge/src/App.css index 6071c72..47b2fab 100644 --- a/chain-forge/src/App.css +++ b/chain-forge/src/App.css @@ -45,3 +45,8 @@ path.react-flow__edge-path:hover { transform: rotate(360deg); } } + +.rich-editor { + min-width: 500px; + min-height: 500px; +} \ No newline at end of file diff --git a/chain-forge/src/App.js b/chain-forge/src/App.js index 50698a2..72c8725 100644 --- a/chain-forge/src/App.js +++ b/chain-forge/src/App.js @@ -15,6 +15,7 @@ import EvaluatorNode from './EvaluatorNode'; import VisNode from './VisNode'; import InspectNode from './InspectorNode'; import ScriptNode from './ScriptNode'; +import CsvNode from './CsvNode'; import './text-fields-node.css'; // State management (from https://reactflow.dev/docs/guides/state-management/) @@ -40,7 +41,8 @@ const nodeTypes = { evaluator: EvaluatorNode, vis: VisNode, inspect: InspectNode, - script: ScriptNode + script: ScriptNode, + csv: CsvNode, }; const connectionLineStyle = { stroke: '#ddd' }; @@ -91,6 +93,11 @@ const App = () => { addNode({ id: 'scriptNode-'+Date.now(), type: 'script', data: {}, position: {x: x-200, y:y-100} }); }; + const addCsvNode = (event) => { + const { x, y } = getViewportCenter(); + addNode({ id: 'csvNode-'+Date.now(), type: 'csv', data: {}, position: {x: x-200, y:y-100} }); + }; + /** * SAVING / LOADING, IMPORT / EXPORT (from JSON) */ @@ -201,6 +208,7 @@ const App = () => { + diff --git a/chain-forge/src/CsvNode.js b/chain-forge/src/CsvNode.js new file mode 100644 index 0000000..1d2c437 --- /dev/null +++ b/chain-forge/src/CsvNode.js @@ -0,0 +1,129 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import { Badge, Text } from '@mantine/core'; +import useStore from './store'; +import NodeLabel from './NodeLabelComponent' +import { IconCsv } from '@tabler/icons-react'; +import { Handle } from 'react-flow-renderer'; + +const CsvNode = ({ data, id }) => { + const setDataPropsForNode = useStore((state) => state.setDataPropsForNode); + const [contentDiv, setContentDiv] = useState(null); + const [isEditing, setIsEditing] = useState(true); + const [csvInput, setCsvInput] = useState(null); + const [countText, setCountText] = useState(null); + + // initializing + useEffect(() => { + if (!data.fields) { + setDataPropsForNode(id, { text: '', fields: [] }); + } + }, []); + + const processCsv = (csv) => { + var matches = csv.match(/(\s*"[^"]+"\s*|\s*[^,]+|,)(?=,|$)/g); + for (var n = 0; n < matches.length; ++n) { + matches[n] = matches[n].trim(); + if (matches[n] == ',') matches[n] = ''; + } + return matches.map(e => e.trim()).filter(e => e.length > 0); + } + + // const processCsv = (csv) => { + // if (!csv) return []; + // // Split the input string by rows, and merge + // var res = csv.split('\n').join(','); + + // // remove all the empty or whitespace-only elements + // return res.split(',').map(e => e.trim()).filter(e => e.length > 0); + // } + + // Handle a change in a text fields' input. + const handleInputChange = useCallback((event) => { + // Update the data for this text fields' id. + let new_data = { 'text': event.target.value, 'fields': processCsv(event.target.value) }; + setDataPropsForNode(id, new_data); + }, [id, setDataPropsForNode]); + + const handKeyDown = useCallback((event) => { + if (event.key === 'Enter') { + setIsEditing(false); + setCsvInput(null); + } + }, []); + + // handling Div Click + const handleDivOnClick = useCallback((event) => { + setIsEditing(true); + }, []); + + const handleOnBlur = useCallback((event) => { + setIsEditing(false); + setCsvInput(null); + }, []); + + // render csv div + const renderCsvDiv = useCallback(() => { + // Take the data.text as csv (only 1 row), and get individual elements + const elements = data.fields; + + // generate a HTML code that highlights the elements + const html = []; + elements.forEach((e, idx) => { + // html.push({e}) + html.push({e}); + if (idx < elements.length - 1) { + html.push(,); + } + }); + + setContentDiv(
+ {html} +
); + setCountText({elements.length} elements); + }, [data.text, handleDivOnClick]); + + // When isEditing changes, add input + useEffect(() => { + if (!isEditing) { + setCsvInput(null); + renderCsvDiv(); + return; + } + if (!csvInput) { + var text_val = data.text || ''; + setCsvInput( +
+