Skip to content

Commit

Permalink
Use .src.md extension instead of .srcmd (#142)
Browse files Browse the repository at this point in the history
* Use .src.md extension instead of .srcmd

* Ignore .src.md in prettier

* Fix tests
  • Loading branch information
benjreinhart authored Jul 17, 2024
1 parent 3808244 commit 56b441f
Show file tree
Hide file tree
Showing 24 changed files with 69 additions and 65 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pnpm*.yaml
packages/api/drizzle/*
**/*.src.md
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Srcbook runs locally on your machine and is fully open-source under the Apache2

Under the hood, Srcbook creates folders on your local machine and provides a web interface (also running locally) as a programming environment.

Srcbooks export to a `.srcmd` format, a superset of markdown. You can easily export Srcbooks into this format from the application, as well as import them. Given that they are a form of markdown, they are very git-friendly.
Srcbooks export to markdown using the `.src.md` extension. These files can easily be shared, versioned, and rendered in any environment that supports Markdown, like your editor or GitHub UI.

To learn more, try out the interactive tutorial which is itself a Srcbook by clicking "Getting Started" when launching the application.

Expand Down
2 changes: 1 addition & 1 deletion packages/api/db/schema.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { randomid } from '@srcbook/shared';

export const configs = sqliteTable('config', {
// Directory where .srcmd files will be stored and searched by default.
// Directory where .src.md files will be stored and searched by default.
baseDir: text('base_dir').notNull(),
defaultLanguage: text('default_language').notNull().default('typescript'),
openaiKey: text('openai_api_key'),
Expand Down
2 changes: 1 addition & 1 deletion packages/api/prompts/cell-generator-javascript.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

You are tasked with generating Srcbook cells for the user.

A Srcbook is a JavaScript notebook following a markdown-compatible format called .srcmd.
A Srcbook is a JavaScript notebook following a markdown-compatible format called `.src.md`.

## Srcbook spec

Expand Down
2 changes: 1 addition & 1 deletion packages/api/prompts/cell-generator-typescript.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

You are tasked with generating Srcbook cells for the user.

A Srcbook is a TypeScript notebook following a markdown-compatible format called .srcmd.
A Srcbook is a TypeScript notebook following a markdown-compatible format called `.src.md`.

## Srcbook spec

Expand Down
6 changes: 3 additions & 3 deletions packages/api/prompts/srcbook-generator.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

You are tasked with generating a srcbook about a topic, which will be requested by the user.

A srcbook is a TypeScript or JavaScript notebook following a markdown-compatible format called .srcmd. It's an interactive and rich way of programming that follows the literate programming idea.
A srcbook is a TypeScript or JavaScript notebook following a markdown-compatible format called .src.md. It's an interactive and rich way of programming that follows the literate programming idea.


Srcbooks are either in javascript, or typescript, and their markdown always starts with an html comment specifying the language, for example for javascript:
Expand Down Expand Up @@ -152,11 +152,11 @@ const token = auth(API_KEY);

## Exporting and sharing Srcbooks

Srcbooks are meant to be collaborative. They export to a friendly `.srcmd` format, which is valid markdown and can be opened in any text editor.
Srcbooks are meant to be collaborative. They export to a friendly `.src.md` format, which is valid markdown and can be opened in any text editor.

You can export Srcbooks by clicking the `Export` link in the top level menu on the left.

You can also import `.srcmd` files directly in this application if you want to run, modify, or re-export them.
You can also import `.src.md` files directly in this application if you want to run, modify, or re-export them.

### Example 2: LangGraph web agent
<!-- srcbook:{"language":"typescript"} -->
Expand Down
9 changes: 5 additions & 4 deletions packages/api/server/http.mts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { readdir } from '../fs-utils.mjs';
import { EXAMPLE_SRCBOOKS } from '../srcbook/examples.mjs';
import { pathToSrcbook } from '../srcbook/path.mjs';
import { isSrcmdPath } from '../srcmd/paths.mjs';

const app: Application = express();

Expand All @@ -40,7 +41,7 @@ router.post('/disk', cors(), async (req, res) => {
try {
const config = await getConfig();
dirname = dirname || config.baseDir;
const entries = await disk(dirname, '.srcmd');
const entries = await disk(dirname, '.src.md');
return res.json({ error: false, result: { dirname, entries } });
} catch (e) {
const error = e as unknown as Error;
Expand Down Expand Up @@ -87,13 +88,13 @@ router.delete('/srcbooks/:id', cors(), async (req, res) => {
return res.json({ error: false, deleted: true });
});

// Import a srcbook from a .srcmd file or srcmd text.
// Import a srcbook from a .src.md file or srcmd text.
router.options('/import', cors());
router.post('/import', cors(), async (req, res) => {
const { path, text } = req.body;

if (path && Path.extname(path) !== '.srcmd') {
return res.json({ error: true, result: 'Importing only works with .srcmd files' });
if (typeof path === 'string' && !isSrcmdPath(path)) {
return res.json({ error: true, result: 'Importing only works with .src.md files' });
}

try {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/session.mts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export async function updateSession(

export async function exportSrcmdFile(session: SessionType, destinationPath: string) {
if (await fileExists(destinationPath)) {
throw new Error(`Cannot export .srcmd file: ${destinationPath} already exists`);
throw new Error(`Cannot export .src.md file: ${destinationPath} already exists`);
}

return fs.writeFile(destinationPath, encode(session.cells, session.metadata, { inline: true }));
Expand Down
6 changes: 3 additions & 3 deletions packages/api/srcbook/examples.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { DIST_DIR, SRCBOOKS_DIR } from '../constants.mjs';

const GETTING_STARTED_SRCBOOK = {
id: '30v2av4eee17m59dg2c29758to',
path: Path.join(DIST_DIR, 'srcbook', 'examples', 'getting-started.srcmd'),
path: Path.join(DIST_DIR, 'srcbook', 'examples', 'getting-started.src.md'),
title: 'Getting started',
description: 'Quick tutorial to explore the basic concepts in Srcbooks.',
get dirname() {
Expand All @@ -19,7 +19,7 @@ const GETTING_STARTED_SRCBOOK = {

const LANGGRAPH_AGENT_SRCBOOK = {
id: 'i72jjpkqepmg5olneffvk7hgto',
path: Path.join(DIST_DIR, 'srcbook', 'examples', 'langgraph-web-agent.srcmd'),
path: Path.join(DIST_DIR, 'srcbook', 'examples', 'langgraph-web-agent.src.md'),
title: 'LangGraph agent',
description: 'Learn to write a stateful agent with memory using LangGraph and Tavily.',
get dirname() {
Expand All @@ -29,7 +29,7 @@ const LANGGRAPH_AGENT_SRCBOOK = {

const INTRO_TO_WEBSOCKETS_SRCBOOK = {
id: 'vnovpn5dbrthpdllvoeqahufc4',
path: Path.join(DIST_DIR, 'srcbook', 'examples', 'websockets.srcmd'),
path: Path.join(DIST_DIR, 'srcbook', 'examples', 'websockets.src.md'),
title: 'Intro to WebSockets',
description: 'Learn to build a simple WebSocket client and server in Node.js.',
get dirname() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ const token = auth(API_KEY);

## Exporting and sharing Srcbooks

Srcbooks are meant to be collaborative. They export to a friendly `.srcmd` format, which is valid markdown and can be opened in any text editor.
Srcbooks are meant to be collaborative. They export to a Markdown file with the `.src.md` extension, which can be rendered in any editor or UI that supports Markdown.

You can export Srcbooks by clicking the `Export` link in the top level menu on the left.

You can also import `.srcmd` files directly in this application if you want to run, modify, or re-export them.
You can also import `.src.md` files directly in this application if you want to run, modify, or re-export them.
Original file line number Diff line number Diff line change
Expand Up @@ -44,24 +44,24 @@ Now, let's define the Agent with LangGraph.js
###### agent.ts

```typescript
import { HumanMessage } from "@langchain/core/messages";
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
import { ChatOpenAI } from "@langchain/openai";
import { END, START, StateGraph, StateGraphArgs } from "@langchain/langgraph";
import { SqliteSaver } from "@langchain/langgraph/checkpoint/sqlite"
import { HumanMessage } from '@langchain/core/messages';
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
import { ChatOpenAI } from '@langchain/openai';
import { END, START, StateGraph, StateGraphArgs } from '@langchain/langgraph';
import { SqliteSaver } from '@langchain/langgraph/checkpoint/sqlite';
// import { MemorySaver } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { ToolNode } from '@langchain/langgraph/prebuilt';

// Define the state interface
interface AgentState {
messages: HumanMessage[];
}

// We'll use a local sqlite DB for memory
export const DB_NAME = 'langgraph_memory.db'
export const DB_NAME = 'langgraph_memory.db';

// Define the graph state
const graphState: StateGraphArgs<AgentState>["channels"] = {
const graphState: StateGraphArgs<AgentState>['channels'] = {
messages: {
value: (x: HumanMessage[], y: HumanMessage[]) => x.concat(y),
default: () => [],
Expand All @@ -75,13 +75,13 @@ const toolNode = new ToolNode<AgentState>(tools);
const model = new ChatOpenAI({ model: 'gpt-4o', temperature: 0 }).bindTools(tools);

// Define the function that determines whether to continue or not
function shouldContinue(state: AgentState): "tools" | typeof END {
function shouldContinue(state: AgentState): 'tools' | typeof END {
const messages = state.messages;
const lastMessage = messages[messages.length - 1];

// If the LLM makes a tool call, then we route to the "tools" node
if (lastMessage.additional_kwargs.tool_calls) {
return "tools";
return 'tools';
}
// Otherwise, we stop (reply to the user)
return END;
Expand All @@ -98,11 +98,11 @@ async function callModel(state: AgentState) {

// Define a new graph
const workflow = new StateGraph<AgentState>({ channels: graphState })
.addNode("agent", callModel)
.addNode("tools", toolNode)
.addEdge(START, "agent")
.addConditionalEdges("agent", shouldContinue)
.addEdge("tools", "agent");
.addNode('agent', callModel)
.addNode('tools', toolNode)
.addEdge(START, 'agent')
.addConditionalEdges('agent', shouldContinue)
.addEdge('tools', 'agent');

// Initialize memory to persist state between graph runs
export const memory = SqliteSaver.fromConnString(DB_NAME);
Expand All @@ -112,27 +112,26 @@ export const memory = SqliteSaver.fromConnString(DB_NAME);
// This compiles it into a LangChain Runnable.
// Note that we're (optionally) passing the memory when compiling the graph
export const app = workflow.compile({ checkpointer: memory });

```

Now that we've built our app, let's invoke it to first get the weather in SF:

###### sf-weather.ts

```typescript
import {app} from './agent.ts';
import { HumanMessage } from "@langchain/core/messages";
import { app } from './agent.ts';
import { HumanMessage } from '@langchain/core/messages';

// Reference a thread
const thread = { configurable: { thread_id: "42" }};
const thread = { configurable: { thread_id: '42' } };

// Use the Runnable
const finalState = await app.invoke(
{ messages: [new HumanMessage("what is the weather in sf")] },
thread
{ messages: [new HumanMessage('what is the weather in sf')] },
thread,
);

console.log(finalState.messages[finalState.messages.length - 1].content)
console.log(finalState.messages[finalState.messages.length - 1].content);
```

Now when we pass the same `thread_id`, in this case `"42"`, the conversation context is retained via the saved state that we've set in a local sqliteDB (i.e. stored list of messages).
Expand All @@ -142,12 +141,12 @@ Also, in this next example, we demonstrate streaming output.
###### ny-weather.ts

```typescript
import {app} from './agent.ts';
import { app } from './agent.ts';
import { HumanMessage } from '@langchain/core/messages';

const nextState = await app.invoke(
{ messages: [new HumanMessage("what about ny")] },
{ configurable: { thread_id: "42"} }
{ messages: [new HumanMessage('what about ny')] },
{ configurable: { thread_id: '42' } },
);

console.log(nextState.messages[nextState.messages.length - 1].content);
Expand All @@ -160,7 +159,7 @@ The memory was saved in the sqlite db `./langGraph.db`. If you want to clear it,
###### clear.ts

```typescript
import {DB_NAME} from './agent.ts';
import { DB_NAME } from './agent.ts';
import fs from 'node:fs';
// I can't find good documentation on the memory module, so let's apply the nuclear method

Expand Down
4 changes: 2 additions & 2 deletions packages/api/srcbook/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function writeReadmeToDisk(
}

/**
* Creates a srcbook directory from a .srcmd file.
* Creates a srcbook directory from a .src.md file.
*/
export async function importSrcbookFromSrcmdFile(srcmdPath: string) {
// Check if the user is opening one of the example Srcbooks that comes bundled with the app.
Expand All @@ -96,7 +96,7 @@ export async function importSrcbookFromSrcmdFile(srcmdPath: string) {
}

/**
* Creates a srcbook directory from a srcmd text.
* Creates a srcbook directory from srcmd text.
*/
export async function importSrcbookFromSrcmdText(text: string, directoryBasename?: string) {
const result = decode(text);
Expand Down
6 changes: 3 additions & 3 deletions packages/api/srcmd/decoding.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
import type { DecodeCellsResult, DecodeResult } from './types.mjs';

/**
* This is used to decode a complete .srcmd file.
* This is used to decode a complete .src.md file.
*/
export function decode(contents: string): DecodeResult {
// First, decode the markdown text into tokens.
Expand Down Expand Up @@ -46,10 +46,10 @@ export function decode(contents: string): DecodeResult {
}

/**
* This is used to decode a subset of a .srcmd file.
* This is used to decode a subset of a .src.md file.
*
* For example, we generate a subset of a Srcbook (1 or more cells) using AI.
* When that happens, we do not have the entire .srcmd contents, so we need
* When that happens, we do not have the entire .src.md contents, so we need
* to ignore some aspects of it, like parsing the metadata.
*/
export function decodeCells(contents: string): DecodeCellsResult {
Expand Down
3 changes: 3 additions & 0 deletions packages/api/srcmd/paths.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isSrcmdPath(path: string) {
return path.endsWith('.src.md');
}
4 changes: 2 additions & 2 deletions packages/api/srcmd/types.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export type DecodeSuccessResult = {
metadata: SrcbookMetadataType;
};

// This represents the result of decoding a complete .srcmd file.
// This represents the result of decoding a complete .src.md file.
export type DecodeResult = DecodeErrorResult | DecodeSuccessResult;

// This represents the result of decoding a subset of content from a .srcmd file.
// This represents the result of decoding a subset of content from a .src.md file.
export type DecodeCellsResult = DecodeErrorResult | Omit<DecodeSuccessResult, 'metadata'>;
2 changes: 1 addition & 1 deletion packages/api/test/srcmd.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('encoding and decoding srcmd files', () => {
const languagePrefix = '<!-- srcbook:{"language": "javascript"} -->\n\n';

beforeAll(async () => {
srcmd = await getRelativeFileContents('srcmd_files/srcbook.srcmd');
srcmd = await getRelativeFileContents('srcmd_files/srcbook.src.md');
});

it('is an error when there is no title', () => {
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/api/utils.mts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function disk(dirname: string, ext: string) {

const entries = results
.filter((entry) => {
return entry.isDirectory() || Path.extname(entry.name) === ext;
return entry.isDirectory() || entry.name.endsWith(ext);
})
.map((entry) => {
return {
Expand Down
4 changes: 2 additions & 2 deletions packages/web/src/components/drag-and-drop-srcmd-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function Modal(props: { open: boolean }) {
<div className="flex-1 flex flex-col items-center justify-center gap-2">
<strong className="text-lg font-semibold leading-tight">Open Srcbook</strong>
<p className="text-tertiary-foreground">
Drop <code className="code">.srcmd</code> file to open
Drop <code className="code">.src.md</code> file to open
</p>
</div>
</div>
Expand All @@ -57,7 +57,7 @@ export function DragAndDropSrcmdModal(props: { children: React.ReactNode }) {
const file = files[0];

// TODO: Error handling
if (!file.name.endsWith('.srcmd')) {
if (!file.name.endsWith('.src.md')) {
return;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/web/src/components/file-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ function FsEntryItem({

export function ExportLocationPicker(props: { onSave: (directory: string, path: string) => void }) {
const filenameRef = useRef<HTMLInputElement | null>(null);
const [filename, setFilename] = useState('untitled.srcmd');
const [filename, setFilename] = useState('untitled.src.md');
const [fsResult, setFsResult] = useState<FsObjectResultType>({ dirname: '', entries: [] });

function onDiskResponse({ result }: DiskResponseType) {
Expand All @@ -173,11 +173,11 @@ export function ExportLocationPicker(props: { onSave: (directory: string, path:

const el = filenameRef.current;
if (el) {
setTimeout(() => el.setSelectionRange(0, el.value.length - '.srcmd'.length), 5);
setTimeout(() => el.setSelectionRange(0, el.value.length - '.src.md'.length), 5);
}
});

const validFilename = /.+\.srcmd$/.test(filename);
const validFilename = /.+\.src\.md$/.test(filename);

return (
<div className="space-y-4 w-full">
Expand Down
Loading

0 comments on commit 56b441f

Please sign in to comment.