Basic Examples

Exported source
# importing dependencies
import re
import pandas as pd
from pandas import DataFrame
from pathlib import Path
from spannerlib.utils import load_env
from spannerlib import get_magic_session,Session,Span
# load openAI api key
load_env()

This tutorials aim to show how to use the spannerlib framework for simple use cases. To illustrate the simplicity of using spannerlib, all required IE functions will be implemented from scratch.

Finding identical sentences in a corpus of documents

TLDR

%%spannerlog

# split documents into sentences with the split ie function
Sents(doc_id,sent)<-
    Docs(doc_id,text),split(text)->(sent).

# get all pairs of equal sentences using the eq_content_spans ie function
# make sure we only get unique pairs by using the span_lt ie function to avoid symmetry
EqualSentsUnique(doc_id1,sent1,doc_id2,sent2)<-
    Sents(doc_id1,sent1),
    Sents(doc_id2,sent2),
    span_lt(sent1,sent2)->(True),
    eq_content_spans(sent1,sent2)->(True).

Walkthrough

In this example, we would like to get a collection of documents. And find identical sentences among them. For example, given the following documents:

input_documents = pd.DataFrame([
    ('doc1', 'The quick brown fox jumps over the lazy dog. Im walking on Sunshine.'),
    ('doc2', 'Im walking on Sunshine. Lorem ipsum. Im walking on Sunshine.'),
    ('doc3', 'All you need is love. The quick brown fox jumps over the lazy dog.'),
])
input_documents
0 1
0 doc1 The quick brown fox jumps over the lazy dog. I...
1 doc2 Im walking on Sunshine. Lorem ipsum. Im walkin...
2 doc3 All you need is love. The quick brown fox jump...

We would like to compute that the first sentence of doc1 is equal to the second sentence of doc3 etc..

Building IE functions

To do so, we need 2 IE functions: * One that extracts the Span of all sentences from a document, called split * The other which would let us know when sentences have identical content but are not actually the same sentence. * this will require them to have the same content when ignoring whitespace * but not be equal spans. Lets call this function eq_content_spans

Lets implement them and register them to our session object. We will use the Span class to save indexed substrings.


source

split

 split (text)
Exported source
# this implementation is naive
# the standard library has a rgx_split ie function that does this in a more efficient way
def split(text):
    split_indices = [ pos for pos,char in enumerate(text) if char == '.' ]
    start = 0
    for pos,char in enumerate(text):
        if char == '.':
            yield Span(text, start, pos)
            start = pos+1

source

eq_content_spans

 eq_content_spans (span1, span2)
Exported source
def eq_content_spans(span1, span2):
    # notice that we are yielding a boolean value
    yield span1 != span2 and str(span1).strip() == str(span2).strip()
print(list(split('The quick brown fox jumps over the lazy dog. Im walking on Sunshine.')))
[[@3ba775,0,43) "The quick ...", [@3ba775,44,67) " Im walkin..."]

Building spannerlog rules

# we register the functions and their input and output schema
sess = get_magic_session()
sess.register('split', split, [str],[Span])
sess.register('eq_content_spans', eq_content_spans,[Span,Span],[bool])

Now let us import our data

sess.import_rel('Docs', input_documents)

Lets make sure we can see our data in using Spannerlog

?Docs(doc_id,text)
'?Docs(doc_id,text)'
doc_id text
doc1 The quick brown fox jumps over the lazy dog. Im walking on Sunshine.
doc2 Im walking on Sunshine. Lorem ipsum. Im walking on Sunshine.
doc3 All you need is love. The quick brown fox jumps over the lazy dog.

Now let us build rules that allow us to find identical sentences:

# this rule gives us
Sents(doc_id,sent)<-
    Docs(doc_id,text),split(text)->(sent).
?Sents(doc_id,sent)

# this rule find equal pairs of sentences
EqualSents(doc_id1,sent1,doc_id2,sent2)<-
    Sents(doc_id1,sent1),
    Sents(doc_id2,sent2),
    eq_content_spans(sent1,sent2)->(True).
?EqualSents(doc_id1,sent1,doc_id2,sent2)
'?Sents(doc_id,sent)'
doc_id sent
doc1 [@3ba775,0,43) "The quick ..."
doc1 [@3ba775,44,67) " Im walkin..."
doc2 [@06bc2d,0,22) "Im walking..."
doc2 [@06bc2d,23,35) " Lorem ips..."
doc2 [@06bc2d,36,59) " Im walkin..."
doc3 [@9c32df,0,20) "All you ne..."
doc3 [@9c32df,21,65) " The quick..."
'?EqualSents(doc_id1,sent1,doc_id2,sent2)'
doc_id1 sent1 doc_id2 sent2
doc1 [@3ba775,0,43) "The quick ..." doc3 [@9c32df,21,65) " The quick..."
doc1 [@3ba775,44,67) " Im walkin..." doc2 [@06bc2d,0,22) "Im walking..."
doc1 [@3ba775,44,67) " Im walkin..." doc2 [@06bc2d,36,59) " Im walkin..."
doc2 [@06bc2d,0,22) "Im walking..." doc1 [@3ba775,44,67) " Im walkin..."
doc2 [@06bc2d,0,22) "Im walking..." doc2 [@06bc2d,36,59) " Im walkin..."
doc2 [@06bc2d,36,59) " Im walkin..." doc1 [@3ba775,44,67) " Im walkin..."
doc2 [@06bc2d,36,59) " Im walkin..." doc2 [@06bc2d,0,22) "Im walking..."
doc3 [@9c32df,21,65) " The quick..." doc1 [@3ba775,0,43) "The quick ..."

Oops, handling symmetric queries

Notice that we got each pair twice. This is because while we think of a sentence pair as a set with two sentences, the tuples (x,y) and (y,x) are actually different. To remedy this we can limit our pairs to pairs where the first sentence is in a smaller position (ie position in memory).

We can do that by introducing a span_lt ie function


source

span_lt

 span_lt (span1, span2)
Exported source
def span_lt(span1, span2):
    yield span1 < span2
sess.register('span_lt', span_lt, [Span,Span],[bool])
EqualSentsUniqe(doc_id1,sent1,doc_id2,sent2)<-
    Sents(doc_id1,sent1),
    Sents(doc_id2,sent2),
    span_lt(sent1,sent2)->(True),
    eq_content_spans(sent1,sent2)->(True).
    
?EqualSentsUniqe(doc_id1,sent1,doc_id2,sent2)
'?EqualSentsUniqe(doc_id1,sent1,doc_id2,sent2)'
doc_id1 sent1 doc_id2 sent2
doc2 [@06bc2d,0,22) "Im walking..." doc1 [@3ba775,44,67) " Im walkin..."
doc2 [@06bc2d,0,22) "Im walking..." doc2 [@06bc2d,36,59) " Im walkin..."
doc2 [@06bc2d,36,59) " Im walkin..." doc1 [@3ba775,44,67) " Im walkin..."
doc3 [@9c32df,21,65) " The quick..." doc1 [@3ba775,0,43) "The quick ..."

Building LLM agents using spannerlib

Motivation

Most LLM based applications do not include a single call to an LLM, but are rather built using LLM agents. LLM agents are programs that * Wrap LLMs in control logic. * Either deterministic * Or decided by an LLM * Offloads structured reasoning to tools, whose output is fed into some of the Prompts that are used by the agent’s LLMs.

In this tutorial we will show how to build a simple agent that might be used to build the backend of a more nuanced ChatGPT-like like system.

Our agent takes: * A question from a user

And has previous knowledge encoding * How to best answer questions on different topics. * How to a user prefers his answers to be formatted.

For example, given the question “How do we measure the distance between stars” we might note that in cases of scientific questions, it is better to answer using mathematical notation and formulas, rather than pure english. Moreover, some users would like the answer to be styled in an academic way, with dense prose ending with citations, like so:

The distance between stars is measured in astronomical units (AU), parsecs (pc), or light-years (ly).

  1. Astronomical Unit (AU): An AU is the average distance between the Earth and the Sun, which is approximately 93 million miles or 150 million kilometers. It is more commonly used to measure distances within our solar system.

  2. Parsec (pc): A parsec is a unit of distance used in astronomy, defined as the distance at which an object would have a parallax angle of one arcsecond. One parsec is equal to approximately 3.26 light-years or 3.09 x 10^13 kilometers.

  3. Light-year (ly): A light-year is the distance that light travels in one year, which is approximately 9.46 trillion kilometers or about 5.88 trillion miles.

To measure the distance to a star, astronomers use methods such as parallax and spectroscopic parallax. Parallax involves observing a star from two different points in Earth’s orbit and measuring the apparent shift in position. Spectroscopic parallax uses the star’s spectral type and luminosity to estimate its distance.

In summary, astronomers use a combination of trigonometric and spectroscopic methods to measure the distance between stars in order to better understand the vastness of our universe.

References: 1. Carroll, B. W., & Ostlie, D. A. (2007). An introduction to modern astrophysics (2nd ed.). Pearson Addison-Wesley. 2. Bennett, J., Donahue, M., Schneider, N., & Voit, M. (2014). The cosmic perspective (7th ed.). Pearson.
While other users will like a shorter answer in simple language like so:

One common way to measure the distance between stars is through parallax. Parallax is the apparent shift in position of an object when viewed from two different points.

The parallax angle, denoted by p, is the angle formed between a line from the observer to the nearer star and a line from the observer to the farther star. The parallax angle is related to the distance to the star (d) by the formula:

d = 1 / p

where d is the distance to the star in parsecs (pc) and p is the parallax angle in arcseconds (“).

A parsec is a unit of distance often used in astronomy, equal to about 3.26 light-years. So when we measure the parallax angle of a star, we can use the formula above to calculate its distance from Earth.

Problem definition

So given: * A relation of the form (user,question) * A relation of the form (topic,topicSpecificIntructions) * A relation of the form (user,userSpecificInstructions)

We would like to output for each user and question, an answer that fits both the user’s specified preferences and the instructions fitting for that topic.

Defining our IE functions

For building the agent we need 2 basic building blocks as IE functions: * an llm function of the form llm(model:str,prompt:str)->(answer:str) * a printf like function format for formatting a prompt from template and other strings which will be of the form format(template:str,s_1:str,...s_n:str)->(prompt:str)

The following section will involve technical details such as data type conversions and interfacing with OpenAI’s API. If this is not of interest to the reader, please skip to the next section.

TLDR

Basic call to LLM

%%spannerlog

model = 'gpt-3.5-turbo'
prompt = "user: sing it with me, love, what is it good for?"

TestLLM(answer)<-
    llm($model,$prompt)->(answer).

?TestLLM(answer)

Style and topic based completion

%%spannerlog
model = 'gpt-3.5-turbo'
# asking the llm to generate the topic based on the question
TopicSelection(question ,topic)<-
    Questions(user,question),
    format($topic_selection_template,question)->(prompt),
    llm($model,prompt)->(topic).

# composing a prompt based on topic and user preference 
# and calling an llm to generate the answer
StyleBasedQA(question,user,answer)<-
    Questions(user,question),
    TopicSelection(question,topic),
    UserAnswerPreference(user,user_style),
    TopicSpecificStyle(topic,topic_style),
    format($custom_style_prompt,topic_style,user_style,question)->(prompt),
    llm($model,prompt)->(answer).

%%python
sess.export('?StyleBasedQA(question,user,answer)')

Walkthrough

Here is the implemetation:

To implement the llm ie function, we need to wrap some LLM api as an IE function. We want an ie function that takes a string and returns a string. Since most LLM api expect a dict of the form {'role':role,'content':message} we will write converters that parse them from prompt strings of the form

role: content
role: content

source

llm

 llm (model, question)
Exported source
from spannerlib.ie_func.basic import rgx_split
from functools import cache
import openai
from joblib import Memory
memory = Memory("cachedir", verbose=0)
Exported source
@cache
def _get_client():
    return openai.Client()

# we use the rgx_split function to split the string into messages
def _str_to_messages (string_prompt):
    return [
        {
            'role': str(role).replace(': ',''),
            'content': str(content)
        } for role,content in rgx_split('system:\s|assistant:\s|user:\s', string_prompt.strip())
    ]
def _messages_to_string(msgs):
    return ''.join([f"{msg['content']}" for msg in msgs])

# the specific API we are going to call using the messages interface
def _openai_chat(model, messages):
    client = _get_client()
    respone = client.chat.completions.create(
        model=model,
        messages=messages,
        seed=42
    )
    return [dict(respone.choices[0].message)]

# we disk cache our function to spare my openAI credits
@memory.cache
def llm(model, question):
    q_msgs = _str_to_messages(question)
    a_msgs = _openai_chat(model, q_msgs)
    answer = _messages_to_string(a_msgs)
    # avoid bad fromatting of the answer in the notebook due to nested code blocks
    answer = answer.replace('```','')
    return [answer]
llm('gpt-3.5-turbo','user: Hello, who are you?')
['Hello! I am a AI-powered virtual assistant designed to help and provide information to users. How can I assist you today?']

Now we can register the llm function as an ie function and use it from in spannerlog code.

Now let see how we can use spannerlib to build a simple pipeline that varries the LLM prompt in a data dependant way. Imagine that we have a chatbot that we want to act differently based on the topic of conversation and the user preference.

To enable us to format prompts from data, we will use a prompt formatting function with a printf like syntax. It is available in spannerlib’s stdlib, but we will reproduce it here.


source

format_ie

 format_ie (f_string, *params)
Exported source
def format_ie(f_string,*params):
    yield f_string.format(*params)

# note that since the schema is dynamic we need to define a function that returns the schema based on the arity
string_schema = lambda x: ([str]*x)
sess.register('llm', llm, [str,str],[str])
sess.register('format', format_ie, string_schema,[str])

Seeing our llm IE function in action

model = 'gpt-3.5-turbo'
prompt = "user: sing it with me, love, what is it good for?"

TestLLM(answer)<-
    llm($model,$prompt)->(answer).

?TestLLM(answer)
'?TestLLM(answer)'
answer
Absolutely nothing!

Example data

We will model our data as follows:

# a binary relation that stores how the user prefers the formatted
user_answer_preference = pd.DataFrame([
    ('Bob', 'please answer in prose. Make sure you add references in the end like a bibliography.'),
    ('Joe', 'Prose is hard for me to read quickly. Format the answer in bullet points.'),
    ('Sally', 'Try to avoid complicated jargon and use simple language.'),
])

# relation that stores per topic style
topic_specific_style = pd.DataFrame([
    ('history', 'please answer in a narrative form, use shakespearing language and lots of examples'),
    ('science', 'Use mathematical notation and formulas to explain the concepts.'),
    ('hiphop', 'introduce the answer with a rap verse.'),
    ('other', 'Be polite and neutral in your answer. Avoid controversial topics.'),
])

sess.import_rel('UserAnswerPreference', user_answer_preference)
sess.import_rel('TopicSpecificStyle', topic_specific_style)

Buliding the agent logic in spannerlog

Now we will build our pipeline as follows: * We will make an llm call to help us decide the topic of the question * Based on the topic and the user, we will formulate the final prompt for the llm to answer.

prompts = pd.DataFrame([
    ('topic_selection',
"""
system: Please select a topic from the following list: [history, science, hiphop, other]
based on the question provided by the user. You are only allowed to say the topic name, nothing else.

user: {}
"""),
    ('custom_style_prompt',
"""
system: Answer the question of the user in the following style:

topic specific style instructions: {}

user specific style instructions: {}

user: {}
"""
)
])
sess.import_rel('Prompts', prompts)
prompts
0 1
0 topic_selection system: Please select a topic from the follow...
1 custom_style_prompt system: Answer the question of the user in th...
?Prompts(prompt_id,prompt)
'?Prompts(prompt_id,prompt)'
prompt_id prompt
custom_style_prompt system: Answer the question of the user in the following style: topic specific style instructions: {} user specific style instructions: {} user: {}
topic_selection system: Please select a topic from the following list: [history, science, hiphop, other] based on the question provided by the user. You are only allowed to say the topic name, nothing else. user: {}

Now given a relation of user and question

questions= pd.DataFrame([
    ('Bob', 'Who won the civil war?'),
    ('Joe', 'Who won the civil war?'),
    ('Sally', 'Who won the civil war?'),
    ('Bob', 'How do we measure the distance between stars?'),
    ('Joe', 'How do we measure the distance between stars?'),
    ('Sally', 'How do we measure the distance between stars?'),
    ('Bob', 'Who are the most well known rappers?'),
    ('Joe', 'Who are the most well known rappers?'),
    ('Sally', 'Who are the most well known rappers?'),
])
sess.import_rel('Questions', questions)

Now lets combine all of this into a pipeline. We will break our pipeline into more debugable bits, and add some extra logical variables in the rule heads for ease of inspection.

model = 'gpt-3.5-turbo'
TopicPrompt(q,p)<-
    Questions(user,q),
    Prompts('topic_selection',template),
    format(template,q)->(p).
    
?TopicPrompt(q,p)

TopicSelection(q,t)<-
    TopicPrompt(q,p),
    llm($model,p)->(t).

?TopicSelection(q,t)

StylePrompt(q,p,topic,user)<-
    Questions(user,q),
    TopicSelection(q,topic),
    UserAnswerPreference(user,user_style),
    TopicSpecificStyle(topic,topic_style),
    Prompts('custom_style_prompt',prompt_template),
    format(prompt_template,topic_style,user_style,q)->(p).


Style_Based_QA(question,topic,user,prompt,answer)<-
    StylePrompt(question,prompt,topic,user),
    llm($model,prompt)->(answer).
'?TopicPrompt(q,p)'
q p
How do we measure the distance between stars? system: Please select a topic from the following list: [history, science, hiphop, other] based on the question provided by the user. You are only allowed to say the topic name, nothing else. user: How do we measure the distance between stars?
Who are the most well known rappers? system: Please select a topic from the following list: [history, science, hiphop, other] based on the question provided by the user. You are only allowed to say the topic name, nothing else. user: Who are the most well known rappers?
Who won the civil war? system: Please select a topic from the following list: [history, science, hiphop, other] based on the question provided by the user. You are only allowed to say the topic name, nothing else. user: Who won the civil war?
'?TopicSelection(q,t)'
q t
How do we measure the distance between stars? science
Who are the most well known rappers? hiphop
Who won the civil war? history

Executing our agent and returning the results to python

completions = sess.export('?Style_Based_QA(question,topic,user,prompt,answer)')
completions
question topic user prompt answer
0 How do we measure the distance between stars? science Bob system: Answer the question of the user in th... The distance between stars is measured in astr...
1 How do we measure the distance between stars? science Joe system: Answer the question of the user in th... - To measure the distance between stars, astro...
2 How do we measure the distance between stars? science Sally system: Answer the question of the user in th... To measure the distance between stars, astrono...
3 Who are the most well known rappers? hiphop Bob system: Answer the question of the user in th... Straight outta Compton, a city so gritty, Let ...
4 Who are the most well known rappers? hiphop Joe system: Answer the question of the user in th... Yo, when it comes to rappers, there's a whole ...
5 Who are the most well known rappers? hiphop Sally system: Answer the question of the user in th... Yo, when it comes to rappers, there's a whole ...
6 Who won the civil war? history Bob system: Answer the question of the user in th... In sooth, fair user, the tale of the Civil War...
7 Who won the civil war? history Joe system: Answer the question of the user in th... - The Civil War, dear user, was a bloody confl...
8 Who won the civil war? history Sally system: Answer the question of the user in th... In sooth, good sir, the tale of the Civil War ...

And as you can see, we have our agent running.

for (question,topic,user,prompt,answer) in completions.itertuples(name=None, index=False):
    print(f"{user}: {question}\nassistant: {answer}\n\n")
    print("="*80)
Bob: How do we measure the distance between stars?
assistant: The distance between stars is measured in astronomical units (AU), parsecs (pc), or light-years (ly). 

1. **Astronomical Unit (AU)**: An AU is the average distance between the Earth and the Sun, which is approximately 93 million miles or 150 million kilometers. It is more commonly used to measure distances within our solar system.

2. **Parsec (pc)**: A parsec is a unit of distance used in astronomy, defined as the distance at which an object would have a parallax angle of one arcsecond. One parsec is equal to about 3.26 light-years or 3.09 x 10^13 kilometers.

3. **Light-year (ly)**: A light-year is the distance that light travels in one year, which is approximately 9.46 trillion kilometers or about 5.88 trillion miles.

To measure the distance to a star, astronomers use methods such as parallax and spectroscopic parallax. Parallax involves measuring the apparent shift of a star against its background as the Earth orbits the Sun. Spectroscopic parallax measures the star's luminosity and temperature to determine its distance.

In summary, the distance between stars is measured in AU, parsecs, or light-years, using methods such as parallax and spectroscopic techniques.

References:
- "Astronomical Unit." NASA, https://solarsystem.nasa.gov/asteroids-comets-and-meteors/comets/overview/. Accessed 15 Dec. 2022.
- "Parsec." European Southern Observatory, https://www.eso.org/public/usa/. Accessed 15 Dec. 2022.
- "Light-Year." European Southern Observatory, https://www.eso.org/public/usa/. Accessed 15 Dec. 2022.


================================================================================
Joe: How do we measure the distance between stars?
assistant: - To measure the distance between stars, astronomers use a method called parallax, which is based on trigonometry.
- Parallax involves observing a star from two different points in Earth's orbit and measuring the angle between the star and distant background stars. 
- By knowing the distance between the two observation points (Earth's orbit), the angle of parallax, and some basic trigonometry, astronomers can calculate the distance to the star. 
- The formula used is d = 1 / tan(p), where d is the distance to the star in parsecs and p is the parallax angle in arcseconds.
- By measuring the parallax angle with precision instruments, astronomers can determine the distance to stars within a certain range of accuracy.


================================================================================
Sally: How do we measure the distance between stars?
assistant: To measure the distance between stars, astronomers use a method called parallax. Parallax is the apparent shift in the position of an object when viewed from different angles. With parallax, astronomers measure the angle subtended by the star's position at different points in the Earth's orbit. The distance to the star can be calculated using the formula:

\[ D = \frac{1}{\text{parallax angle}} \]

where \( D \) is the distance to the star. The parallax angle is usually measured in arcseconds. The smaller the parallax angle, the greater the distance to the star. This method allows astronomers to measure distances to nearby stars accurately.


================================================================================
Bob: Who are the most well known rappers?
assistant: Straight outta Compton, a city so gritty,
Let me tell you about the rappers who made it to the top, no pity.
From the East Coast to the West, they've all left a mark,
Icons like Tupac, Biggie, and Jay-Z, they're the ones that spark.

Other legends include Eminem, with his lyrical prowess,
And Kanye West, pushing boundaries, never settling for less.
We can't forget about the women who've paved the way,
Nicki Minaj, Cardi B, Queen Latifah, they slay.

But the rap game is vast, with talents galore,
From Kendrick Lamar to Lil Wayne, they all score.
So if you're looking for names that shine bright,
These rappers will definitely be in sight.

References:
1. Tupac Shakur - Biography. (n.d.). Retrieved from https://www.biography.com/musician/tupac-shakur
2. The Notorious B.I.G. - Biography. (n.d.). Retrieved from https://www.biography.com/musician/the-notorious-big
3. Jay-Z - Biography. (n.d.). Retrieved from https://www.biography.com/musician/jay-z
4. Eminem - Biography. (n.d.). Retrieved from https://www.biography.com/musician/eminem
5. Kanye West - Biography. (n.d.). Retrieved from https://www.biography.com/musician/kanye-west
6. Nicki Minaj - Biography. (n.d.). Retrieved from https://www.biography.com/musician/nicki-minaj


================================================================================
Joe: Who are the most well known rappers?
assistant: Yo, when it comes to rappers, there's a whole crew,
Here are some of the most well-known for you:

- Eminem: Known for his lyrical genius and raw talent
- Jay-Z: Business mogul and rap legend with a smooth flow
- Tupac Shakur: Iconic figure known for his powerful lyrics and impact on the industry
- Notorious B.I.G: Legendary rapper with a unique flow and storytelling abilities
- Kendrick Lamar: Modern-day artist known for his socially conscious lyrics and innovative sound
- Drake: Chart-topping artist with a versatile style and massive commercial success
- Kanye West: Controversial figure known for pushing boundaries and redefining the genre


================================================================================
Sally: Who are the most well known rappers?
assistant: Yo, when it comes to rappers, there's a whole lot in the game,
From OGs like Tupac and Biggie, to new stars on their rise to fame.
Eminem's known for his lyrical prowess and flow,
While Jay-Z's the blueprint for success, yo.

Drake stays topping charts with hits in every season,
And Kendrick Lamar speaks truth, his words have reason.
Don't forget about Cardi B, the queen of the Bronx,
And Nicki Minaj, always bringing heat, never clocks.

These are just a few of the names in the scene,
But there's plenty more with rhymes so mean.
So if you're looking to vibe to some sick beats,
Check out these rappers and feel their heat.


================================================================================
Bob: Who won the civil war?
assistant: In sooth, fair user, the tale of the Civil War is a tumultuous one, with many a battle fought and blood shed ere the ultimate victor emerged. Verily, the conflict betwixt the noble Union forces and the valiant Confederate army didst rage for four long years, from the year of our Lord 1861 to 1865.

'Twas in the year of 1865, that the scales of fortune didst tip in favor of the Union, under the gallant leadership of General Ulysses S. Grant. The Confederate forces, led by General Robert E. Lee, didst valiantly defend their cause, but in the end, they couldst not withstand the might of the Union army.

Thus, with the surrender of General Lee at Appomattox Court House on April 9, 1865, the noble Union forces emerged victorious in this bitter conflict. The unity of the nation was preserved, and the scourge of slavery was banished from the land.

So, dear user, it canst be said that the Civil War was won by the Union forces, securing the future of a united and free America.

References:
1. McPherson, James M. "Battle Cry of Freedom: The Civil War Era." Oxford University Press, 1988.
2. Foote, Shelby. "The Civil War: A Narrative." Vintage Books, 1986.


================================================================================
Joe: Who won the civil war?
assistant: - The Civil War, dear user, was a bloody conflict
- In this great struggle, Armies clashed and did afflict
- 'Twas the blue-clad Union and the gray Confederate band
- Seeking victory and power over this vast land
  
- As the years dragged on, battles were fiercely fought
- From Gettysburg to Antietam, each victory dearly bought
- But in the end, the Union did prevail
- And the Confederacy's hopes did in sorrow pale
  
- Abraham Lincoln, the great President of the North
- Led his people through this war, showing his worth
- With Grant and Sherman by his side
- They turned the tide, their foes to chide
  
- So it was the Union, with its starry flag unfurled
- Who emerged victorious, in this tumultuous world
- The Confederacy, defeated and forlorn
- Surrendered at Appomattox, their dreams torn
  
- And so, dear user, the answer is clear
- The Union won the Civil War, without fear
- But the scars of this conflict still remain
- In the history of our nation, causing lasting pain


================================================================================
Sally: Who won the civil war?
assistant: In sooth, good sir, the tale of the Civil War is a tragic and tumultuous one, fraught with strife and sorrow. 'Twas a conflict of great import that pitted brother against brother and father against son, tearing asunder the very fabric of our nation.

As for the victors of this bloody conflict, it was the Union forces led by General Ulysses S. Grant who emerged triumphant over the Confederate army under General Robert E. Lee. 'Twas in the spring of 1865, at the Battle of Appomattox Court House, that General Lee surrendered his sword to General Grant, marking the end of the war and the preservation of the Union.

'Tis a tale of great tragedy and triumph, of sacrifice and suffering, but in the end, the Union was preserved and the promise of liberty and equality for all was upheld. Let us not forget the lessons learned from this dark chapter in our nation's history, and let us strive to heal the wounds of the past and work towards a brighter future for all.


================================================================================