565 čtení
565 čtení

Agent AI, který interpretuje papíry, takže nemusíte: kompletní průvodce stavbou

podle Superlinked18m2025/06/26
Read on Terminal Reader

Příliš dlouho; Číst

Vytvořte výzkumného agenta umělé inteligence v reálném čase pomocí vektorového vyhledávání společnosti Superlinked, který přeskočí složité potrubí RAG tím, že přímo vloží a dotazuje dokumenty, čímž bude výzkum rychlejší, jednodušší a chytřejší.
featured image - Agent AI, který interpretuje papíry, takže nemusíte: kompletní průvodce stavbou
Superlinked HackerNoon profile picture
0-item
1-item


Zjistěte, jak vytvořit agenta AI pro získávání výzkumných dokumentů, vyhledávání a shrnutí

Zjistěte, jak vytvořit agenta AI pro získávání výzkumných dokumentů, vyhledávání a shrnutí

Pro výzkumníky je udržování aktuálního stavu s nejnovějšími poznatky podobné nalezení jehly v hnízdě.Představte si asistenta poháněného umělou inteligencí, který nejen získá nejrelevantnější články, ale také shrnuje klíčové poznatky a odpovídá na vaše konkrétní otázky, a to vše v reálném čase.

Tento článek se zabývá konstrukcí takového výzkumného agenta AI pomocí komplexních schopností vkládání dokumentů společnosti Superlinked.Integrací sémantické a časové relevance eliminujeme potřebu komplexního přerozdělování, což zajišťuje efektivní a přesné získávání informací.

Tento článek se zabývá konstrukcí takového výzkumného agenta AI pomocí komplexních schopností vkládání dokumentů společnosti Superlinked.Integrací sémantické a časové relevance eliminujeme potřebu komplexního přerozdělování, což zajišťuje efektivní a přesné získávání informací.

TL;DR:

Vytvořte výzkumného agenta umělé inteligence v reálném čase pomocí vektorového vyhledávání společnosti Superlinked, který přeskočí složité potrubí RAG tím, že přímo vloží a dotazuje dokumenty, čímž bude výzkum rychlejší, jednodušší a chytřejší.

(Chcete skočit přímo na kód? Podívejte se na open source na GitHub zde. Připraveni vyzkoušet sémantické vyhledávání pro svůj vlastní případ použití agentů?

Podívejte se na open source na Githubuzde.zdezdeJsme tu proPomoc.PomocPomoc

Tento článek ukazuje, jak vytvořit systém agentů pomocí agenta jádra pro zpracování dotazů. Pokud chcete sledovat a spouštět kód v prohlížeči,here’s the colab.

Koloběh.Koloběh

Kde začít s výzkumným asistentem?

Tradičně budování takového systému zahrnuje složitost a značné investice do zdrojů. Vyhledávací systémy obvykle vyhledávají počáteční širokou škálu dokumentů založených na relevanci a následně aplikují sekundární přerozdělovací proces k zdokonalování a přerozdělování výsledků. Zatímco přerozdělování zvyšuje přesnost, výrazně zvyšuje výpočetní složitost, latenci a přehodnocení kvůli rozsáhlému vyhledávání dat, které bylo původně požadováno. Superlinked řeší tuto složitost kombinací strukturovaných číselných a kategorických vkládání se sémantickým textovým vkládáním, což poskytuje komplexní multimodální vektory. Tato metoda výrazně zvyšuje přesnost vyhledávání zachováním atribut

Vytvořte agentový systém s Superlinked

Tento agent AI může dělat tři hlavní věci:

  1. Hledej papíry: Hledejte výzkumné práce podle tématu (např. „kvantové výpočetní techniky“) a poté je řadíte podle relevance a aktuálnosti.
  2. Shrnutí papírů: Shrnutí vyhledávaných papírů do pohledů velikosti kousku.
  3. Odpovědi na otázky: Odpovědi lze extrahovat přímo z konkrétních výzkumných prací na základě cílených dotazů uživatelů.

Superlinked eliminuje potřebu metody re-ranking, protože zlepšuje relevanci vektorového vyhledávání. Superlinked RecencySpace bude použit, který specificky kóduje časové metadata, upřednostňuje nedávné dokumenty během vyhledávání a eliminuje potřebu výpočetně nákladné re-ranking. Například, pokud dva papíry mají stejnou relevanci - ten, který je nejnovější bude hodnocena vyšší.

Krok 1: Nastavení toolboxu

 %pip install superlinked

Aby bylo vše jednodušší a modulárnější, vytvořil jsem třídu abstraktních nástrojů, která zjednoduší proces budování a přidávání nástrojů.

import pandas as pd
import superlinked.framework as sl
from datetime import timedelta
from sentence_transformers import SentenceTransformer
from openai import OpenAI
import os
from abc import ABC, abstractmethod
from typing import Any, Optional, Dict
from tqdm import tqdm
from google.colab import userdata

# Abstract Tool Class
class Tool(ABC):
    @abstractmethod
    def name(self) -> str:
        pass

    @abstractmethod
    def description(self) -> str:
        pass

    @abstractmethod
    def use(self, *args, **kwargs) -> Any:
        pass


# Get API key from Google Colab secrets
try:
    api_key = userdata.get('OPENAI_API_KEY')
except KeyError:
    raise ValueError("OPENAI_API_KEY not found in user secrets. Please add it using Tools > User secrets.")

# Initialize OpenAI Client
api_key = os.environ.get("OPENAI_API_KEY", "your-openai-key")  # Replace with your OpenAI API key
if not api_key:
    raise ValueError("Please set the OPENAI_API_KEY environment variable.")

client = OpenAI(api_key=api_key)
model = "gpt-4"


Krok 2: Pochopení databáze

Tento příklad používá datový soubor obsahující přibližně 10 000 výzkumných článků AI dostupných naKádrováChcete-li to usnadnit, jednoduše spusťte níže uvedenou buňku a automaticky stáhne datovou sadu do vaší pracovní adresáře. Můžete také použít své vlastní zdroje dat, jako jsou výzkumné články nebo jiný akademický obsah. Pokud se rozhodnete tak učinit, vše, co musíte udělat, je mírně upravit design schématu a aktualizovat názvy sloupců.

import pandas as pd

!wget --no-check-certificate 'https://drive.google.com/uc?export=download&id=1FCR3TW5yLjGhEmm-Uclw0_5PWVEaLk1j' -O arxiv_ai_data.csv


Prozatím budeme používat menší podskupinu papírů, abychom věci zrychlili, ale nezapomeňte si vyzkoušet příklad s použitím celé datové sady.Důležitým technickým detailem je, že časové razítka z datové sady budou převedeny z časových razítek řetězce (jako '1993-08-01 00:00:00+00:00') na datetime objekty pandy. Tato konverze je nezbytná, protože nám umožňuje provádět operace datum/čas.

df = pd.read_csv('arxiv_ai_data.csv').head(100)

# Convert to datetime but keep it as datetime (more readable and usable)
df['published'] = pd.to_datetime(df['published'])

# Ensure summary is a string
df['summary'] = df['summary'].astype(str)

# Add 'text' column for similarity search
df['text'] = df['title'] + " " + df['summary']
Debug: Columns in original DataFrame: ['authors', 'categories', 'comment', 'doi', 'entry_id', 'journal_ref' 'pdf_url', 'primary_category', 'published', 'summary', 'title', 'updated']

Pochopení datových sloupců

Níže je stručný přehled klíčových sloupců v naší datové sadě, které budou důležité v následujících krocích:

  1. Publikováno: Datum zveřejnění výzkumného dokumentu.
  2. Shrnutí: Abstrakt papíru, který poskytuje stručný přehled.
  3. entry_id: Jedinečný identifikátor pro každý papír z arXiv.

Pro tuto demonstraci se zaměřujeme konkrétně na čtyři sloupce:entry_id, ,published, ,title, asummaryPro optimalizaci kvality vyhledávání jsou název a shrnutí kombinovány do jediného, komplexního textového sloupce, který tvoří jádro našeho procesu vkládání a vyhledávání.

Poznámka k indexu Superlinked In-Memory: In-memory indexování společnosti Superlinked ukládá naše datové soubory přímo do paměti RAM, což činí vyhledávání mimořádně rychlým, což je ideální pro vyhledávání v reálném čase a rychlé prototypování.

Krok 3: Definování superlinkovaného schématu

Chcete-li jít dál, je zapotřebí schéma mapovat naše data.PaperSchemav klíčových oblastech:

lass PaperSchema(sl.Schema):
    text: sl.String
    published: sl.Timestamp  # This will handle datetime objects properly
    entry_id: sl.IdField
    title: sl.String
    summary: sl.String

paper = PaperSchema()


Definování superlinkovaných prostor pro efektivní zpětné získávání

Nezbytným krokem při organizaci a efektivním vyhledávání datových souborů je definování dvou specializovaných vektorových prostorů: TextSimilaritySpace a RecencySpace.

  1. Textilní prostor

TytoTextSimilaritySpaceje navržen tak, aby kódoval textové informace – jako jsou tituly a abstrakty výzkumných prací – do vektorů. Převedením textu na vkládání tento prostor výrazně zvyšuje snadnost a přesnost sémantických vyhledávání. Je optimalizován speciálně pro efektivní zpracování delších textových sekvencí, což umožňuje přesné srovnání podobnosti mezi dokumenty.

text_space = sl.TextSimilaritySpace(
    text=sl.chunk(paper.text, chunk_size=200, chunk_overlap=50),
    model="sentence-transformers/all-mpnet-base-v2"
)


  1. Recenzní prostor

TytoRecencySpacezachycuje časové metadata, zdůrazňující nedávnost výzkumných publikací. kódováním časových razítek tento prostor přiděluje větší význam novějším dokumentům.V důsledku toho výsledky vyhledávání přirozeně vyvažují relevanci obsahu s daty publikace, což přispívá k nedávným poznatkům.

recency_space = sl.RecencySpace(
    timestamp=paper.published,
    period_time_list=[
        sl.PeriodTime(timedelta(days=365)),      # papers within 1 year
        sl.PeriodTime(timedelta(days=2*365)),    # papers within 2 years
        sl.PeriodTime(timedelta(days=3*365)),    # papers within 3 years
    ],
    negative_filter=-0.25
)


Přemýšlejte o RecencySpace jako o filtru založeném na čase, podobném třídění vašich e-mailů podle data nebo prohlížení příspěvků na Instagramu s nejnovějšími nejprve.

  • Menší časové úseky (například 365 dní) umožňují více granulární roční časové hodnocení.
  • Větší časové úseky (například 1095 dní) vytvářejí širší časové období.

Tytonegative_filterPro jasnější vysvětlení, zvažte následující příklad, kde dva články mají stejný obsah relevance, ale jejich hodnocení bude záviset na jejich data publikace.

Paper A: Published in 1996 
Paper B: Published in 1993

Scoring example:
- Text similarity score: Both papers get 0.8
- Recency score:
  - Paper A: Receives the full recency boost (1.0)
  - Paper B: Gets penalized (-0.25 due to negative_filter)

Final combined scores:
- Paper A: Higher final rank
- Paper B: Lower final rank


Tyto prostory jsou klíčem k tomu, aby byl datový soubor přístupnější a efektivnější. Umožňují jak vyhledávání založené na obsahu, tak vyhledávání založené na čase a jsou velmi užitečné pro pochopení relevance a aktuálnosti výzkumných prací.To poskytuje výkonný způsob, jak datový soubor organizovat a vyhledávat na základě obsahu i času publikování.

Krok 4: Vytvoření indexu

Následně jsou mezery sloučeny do indexu, který je jádrem vyhledávače:

paper_index = sl.Index([text_space, recency_space])

DataFrame je pak mapován do schématu a načten v dávkách (10 papírů najednou) do úložiště v paměti:

# Parser to map DataFrame columns to schema fields
parser = sl.DataFrameParser(
    paper,
    mapping={
        paper.entry_id: "entry_id",
        paper.published: "published",
        paper.text: "text",
        paper.title: "title",
        paper.summary: "summary",
    }
)

# Set up in-memory source and executor
source = sl.InMemorySource(paper, parser=parser)
executor = sl.InMemoryExecutor(sources=[source], indices=[paper_index])
app = executor.run()

# Load the DataFrame with a progress bar using batches
batch_size = 10
data_batches = [df[i:i + batch_size] for i in range(0, len(df), batch_size)]
for batch in tqdm(data_batches, total=len(data_batches), desc="Loading Data into Source"):
    source.put([batch])


Exekutor v paměti je důvodem, proč Superlinked zde září - 1000 papírů se hodí do paměti RAM a dotazy letí bez disků I / O.

Krok 5: Vytvoření dotazu

Následuje vytváření dotazu. To je místo, kde je vytvořena šablona pro vytváření dotazů. Chcete-li to spravovat, potřebujeme šablonu dotazu, která dokáže vyvážit jak relevanci, tak aktuálnost.

# Define the query
knowledgebase_query = (
    sl.Query(
        paper_index,
        weights={
            text_space: sl.Param("relevance_weight"),
            recency_space: sl.Param("recency_weight"),
        }
    )
    .find(paper)
    .similar(text_space, sl.Param("search_query"))
    .select(paper.entry_id, paper.published, paper.text, paper.title, paper.summary)
    .limit(sl.Param("limit"))
)


To nám umožňuje zvolit, zda upřednostnit obsah (relevance_váha) nebo aktuálnost (recency_váha) - velmi užitečná kombinace pro potřeby našeho agenta.

Krok 6: Stavební nástroje

Nyní přichází nástrojová část.

Budeme pracovat na třech nástrojích...

  1. Nástroj pro vyhledávání: Tento nástroj je vytvořen zapojením do indexu Superlinked, což mu umožňuje vytáhnout top 5 papírů na základě dotazu. Vyváží relevanci (1.0 hmotnost) a aktuálnost (0,5 hmotnost) k dosažení cíle „hledat papíry“. Co chceme, je najít papíry, které jsou relevantní pro dotaz. Takže pokud je dotaz: „Jaké kvantové výpočetní papíry byly publikovány mezi lety 1993 a 1994?“, pak nástroj pro vyhledávání tyto papíry vyhledá, shrne je jeden po druhém a vrátí výsledky.
class RetrievalTool(Tool):
    def __init__(self, df, app, knowledgebase_query, client, model):
        self.df = df
        self.app = app
        self.knowledgebase_query = knowledgebase_query
        self.client = client
        self.model = model

    def name(self) -> str:
        return "RetrievalTool"

    def description(self) -> str:
        return "Retrieves a list of relevant papers based on a query using Superlinked."

    def use(self, query: str) -> pd.DataFrame:
        result = self.app.query(
            self.knowledgebase_query,
            relevance_weight=1.0,
            recency_weight=0.5,
            search_query=query,
            limit=5
        )
        df_result = sl.PandasConverter.to_pandas(result)
        # Ensure summary is a string
        if 'summary' in df_result.columns:
            df_result['summary'] = df_result['summary'].astype(str)
        else:
            print("Warning: 'summary' column not found in retrieved DataFrame.")
        return df_result


Další nahoře jeSummarization ToolTento nástroj je určen pro případy, kdy je zapotřebí stručné shrnutí papíru.paper_id, což je ID papíru, který je třeba shrnout.paper_idnení poskytnuta, nástroj nebude fungovat, protože tyto ID jsou požadavkem pro nalezení odpovídajících papírů v databázi.

class SummarizationTool(Tool):
    def __init__(self, df, client, model):
        self.df = df
        self.client = client
        self.model = model

    def name(self) -> str:
        return "SummarizationTool"

    def description(self) -> str:
        return "Generates a concise summary of specified papers using an LLM."

    def use(self, query: str, paper_ids: list) -> str:
        papers = self.df[self.df['entry_id'].isin(paper_ids)]
        if papers.empty:
            return "No papers found with the given IDs."
        summaries = papers['summary'].tolist()
        summary_str = "\n\n".join(summaries)
        prompt = f"""
        Summarize the following paper summaries:\n\n{summary_str}\n\nProvide a concise summary.
        """
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7,
            max_tokens=500
        )
        return response.choices[0].message.content.strip()


Konečně máme naQuestionAnsweringToolTento nástroj řetězceRetrievalToolvyzvednout příslušné dokumenty a pak je použít k zodpovězení otázek. Pokud nejsou nalezeny žádné příslušné dokumenty, které by odpovídaly na otázky, poskytne odpověď založenou na obecných znalostech

class QuestionAnsweringTool(Tool):
    def __init__(self, retrieval_tool, client, model):
        self.retrieval_tool = retrieval_tool
        self.client = client
        self.model = model

    def name(self) -> str:
        return "QuestionAnsweringTool"

    def description(self) -> str:
        return "Answers questions about research topics using retrieved paper summaries or general knowledge if no specific context is available."

    def use(self, query: str) -> str:
        df_result = self.retrieval_tool.use(query)
        if 'summary' not in df_result.columns:
            # Tag as a general question if summary is missing
            prompt = f"""
            You are a knowledgeable research assistant. This is a general question tagged as [GENERAL]. Answer based on your broad knowledge, not limited to specific paper summaries. If you don't know the answer, provide a brief explanation of why.

            User's question: {query}
            """
        else:
            # Use paper summaries for specific context
            contexts = df_result['summary'].tolist()
            context_str = "\n\n".join(contexts)
            prompt = f"""
            You are a research assistant. Use the following paper summaries to answer the user's question. If you don't know the answer based on the summaries, say 'I don't know.'

            Paper summaries:
            {context_str}

            User's question: {query}
            """
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7,
            max_tokens=500
        )
        return response.choices[0].message.content.strip()


Krok 7: Vytvoření agenta jádra

Následuje jaderný agent. Funguje jako centrální ovladač, který zajišťuje hladký a efektivní provoz. Jako základní součást systému, jaderný agent koordinuje komunikaci směrováním dotazů podle jejich záměru, když více agentů pracuje současně. V systémech s jedním agentem, jako je tento, jaderný agent přímo používá příslušné nástroje k efektivnímu řízení úkolů.

class KernelAgent:
    def __init__(self, retrieval_tool: RetrievalTool, summarization_tool: SummarizationTool, question_answering_tool: QuestionAnsweringTool, client, model):
        self.retrieval_tool = retrieval_tool
        self.summarization_tool = summarization_tool
        self.question_answering_tool = question_answering_tool
        self.client = client
        self.model = model

    def classify_query(self, query: str) -> str:
        prompt = f"""
        Classify the following user prompt into one of the three categories:
        - retrieval: The user wants to find a list of papers based on some criteria (e.g., 'Find papers on AI ethics from 2020').
        - summarization: The user wants to summarize a list of papers (e.g., 'Summarize papers with entry_id 123, 456, 789').
        - question_answering: The user wants to ask a question about research topics and get an answer (e.g., 'What is the latest development in AI ethics?').

        User prompt: {query}

        Respond with only the category name (retrieval, summarization, question_answering).
        If unsure, respond with 'unknown'.
        """
        response = self.client.chat.completions.create(
            model=self.model,
            messages=[{"role": "user", "content": prompt}],
            temperature=0.7,
            max_tokens=10
        )
        classification = response.choices[0].message.content.strip().lower()
        print(f"Query type: {classification}")
        return classification

    def process_query(self, query: str, params: Optional[Dict] = None) -> str:
        query_type = self.classify_query(query)
        if query_type == 'retrieval':
            df_result = self.retrieval_tool.use(query)
            response = "Here are the top papers:\n"
            for i, row in df_result.iterrows():
                # Ensure summary is a string and handle empty cases
                summary = str(row['summary']) if pd.notna(row['summary']) else ""
                response += f"{i+1}. {row['title']} \nSummary: {summary[:200]}...\n\n"
            return response
        elif query_type == 'summarization':
            if not params or 'paper_ids' not in params:
                return "Error: Summarization query requires a 'paper_ids' parameter with a list of entry_ids."
            return self.summarization_tool.use(query, params['paper_ids'])
        elif query_type == 'question_answering':
            return self.question_answering_tool.use(query)
        else:
            return "Error: Unable to classify query as 'retrieval', 'summarization', or 'question_answering'."


V této fázi byly všechny součásti systému výzkumných agentů nakonfigurovány.Systém lze nyní inicializovat poskytnutím příslušných nástrojů jádrovému agentu, po kterém bude systém výzkumných agentů plně funkční.

retrieval_tool = RetrievalTool(df, app, knowledgebase_query, client, model)
summarization_tool = SummarizationTool(df, client, model)
question_answering_tool = QuestionAnsweringTool(retrieval_tool, client, model)

# Initialize KernelAgent
kernel_agent = KernelAgent(retrieval_tool, summarization_tool, question_answering_tool, client, model)


Pojďme si ten systém vyzkoušet...

# Test query print(kernel_agent.process_query("Find papers on quantum computing in last 10 years"))

Spuštění tohoto systému aktivujeRetrievalToolZíská příslušné dokumenty na základě relevance i aktuálnosti a vrátí příslušné sloupce.Pokud vrácený výsledek zahrnuje sloupec souhrnu (ukazující, že dokumenty byly získány z datové sady), použije tyto souhrny a vrátí nám je.

Query type: retrieval
Here are the top papers:
1. Quantum Computing and Phase Transitions in Combinatorial Search 
Summary: We introduce an algorithm for combinatorial search on quantum computers that
is capable of significantly concentrating amplitude into solutions for some NP
search problems, on average. This is done by...

1. The Road to Quantum Artificial Intelligence 
Summary: This paper overviews the basic principles and recent advances in the emerging
field of Quantum Computation (QC), highlighting its potential application to
Artificial Intelligence (AI). The paper provi...

1. Solving Highly Constrained Search Problems with Quantum Computers 
Summary: A previously developed quantum search algorithm for solving 1-SAT problems in
a single step is generalized to apply to a range of highly constrained k-SAT
problems. We identify a bound on the number o...

1. The model of quantum evolution 
Summary: This paper has been withdrawn by the author due to extremely unscientific
errors....

1. Artificial and Biological Intelligence 
Summary: This article considers evidence from physical and biological sciences to show
machines are deficient compared to biological systems at incorporating
intelligence. Machines fall short on two counts: fi...


Pojďme zkusit další dotaz, tentokrát, pojďme udělat shrnutí jeden..

print(kernel_agent.process_query("Summarize this paper", params={"paper_ids": ["http://arxiv.org/abs/cs/9311101v1"]}))

Query type: summarization
This paper discusses the challenges of learning logic programs that contain the cut predicate (!). Traditional learning methods cannot handle clauses with cut because it has a procedural meaning. The proposed approach is to first generate a candidate base program that covers positive examples, and then make it consistent by inserting cut where needed. Learning programs with cut is difficult due to the need for intensional evaluation, and current induction techniques may need to be limited to purely declarative logic languages.


Doufám, že tento příklad byl užitečný pro vývoj agentů AI a systémů založených na agentech.repozitářpro budoucí reference, když jsou pro vaše agenty AI zapotřebí přesné možnosti vyhledávání!

Vezměte

Notebookový kód

  • Kombinace sémantické a časové relevance eliminuje složité přerozdělování při zachování přesnosti vyhledávání pro výzkumné práce.
  • Tresty založené na čase (negative_filter=-0.25) upřednostňují nedávný výzkum, pokud mají články podobný obsah relevance.
  • Modulární architektura založená na nástrojích umožňuje specializovaným komponentům zvládat odlišné úkoly (získávání, shromažďování, odpověď na otázky) při zachování soudržnosti systému.
  • Ukládání dat v malých dávkách (batch_size=10) se sledováním pokroku zlepšuje stabilitu systému při zpracování velkých datových souborů výzkumu.
  • Nastavitelné váhy dotazů umožňují uživatelům vyvážit relevanci (1.0) a aktuálnost (0.5) na základě specifických výzkumných potřeb.
  • Součást odpovídání na otázky se elegantně degraduje na obecné znalosti, když není k dispozici papírový kontext, čímž se zabraňuje mrtvým uživatelským zkušenostem.

Aktualizace velkého počtu pravidelně publikovaných výzkumných článků může být náročná a časově náročná.Agentní pracovní postup asistenta AI, který je schopen efektivně lokalizovat relevantní výzkum, shrnout klíčové poznatky a odpovědět na konkrétní otázky z těchto článků, by mohl tento proces výrazně zjednodušit.

Přispěvatelé

  • Vipul Maheshwari, autor
  • Filip Makraduli, recenzent


L O A D I N G
. . . comments & more!

About Author

Superlinked HackerNoon profile picture
Superlinked@superlinked
Turning complex data into vector embeddings for better AI/ML results.

ZAVĚŠIT ZNAČKY

TENTO ČLÁNEK BYL PŘEDSTAVEN V...

Trending Topics

blockchaincryptocurrencyhackernoon-top-storyprogrammingsoftware-developmenttechnologystartuphackernoon-booksBitcoinbooks